/* Backstage by Invision — dashboard styles */

:root {
  --bg:            #1e1e1e;
  --panel:         #2a2a2a;
  --sidebar:       #141414;
  --border:        rgba(255, 255, 255, 0.06);
  --border-strong: rgba(255, 255, 255, 0.13);

  /* Resting boundary for data-entry controls (text inputs, selects,
     textareas). WCAG 1.4.11 requires a UI-component boundary of 3:1; the
     old `transparent` resting border gave inputs no perceivable edge until
     focus. #767676 is 3.16:1 against the --panel #2a2a2a (the lightest
     surface inputs sit on) and higher on the darker listings/sidebar
     surfaces. Tune here if the outlined look needs softening — but don't
     drop below #767676 or it fails 3:1 on the panel. */
  --input-border:  #767676;

  /* Subtle elevated surfaces. Used for "cards inside a panel" (score
     cards, message-history items, description block, threshold form)
     where a faint background lift replaces the old 1px outer border.
     Keeps grouping legible without boxing every element in. */
  --surface-1:     rgba(255, 255, 255, 0.03);
  --surface-2:     rgba(255, 255, 255, 0.05);

  /* Text scale. --text-3 was bumped from #7e7e7e to #a3a3a3 to clear
     WCAG AA (4.5:1) against the panel and listings-panel backgrounds
     for the small uppercase caps that use it (meta-cell dt, section
     heading hints, message timestamps). Still clearly muted relative
     to --text-2 (#b4b4b4) so visual hierarchy is preserved. */
  --text:          #ededed;
  --text-2:        #b4b4b4;
  --text-3:        #a3a3a3;

  --accent:        #ffffff;

  /* --red bumped #ff6b6b → #ff8585 to clear WCAG AA 4.5:1 as text on the
     --red-bg tint (the old value was 4.21:1 on red-bg/panel, used by the
     score-red badge, .btn-danger:hover, dismiss button, danger menu item).
     #ff8585 is 4.98:1 on red-bg and 6.1:1+ on plain surfaces, still clearly
     red. --red-bg keeps the original hue so the tint is unchanged. */
  --red:           #ff8585;
  --red-bg:        rgba(255, 107, 107, 0.14);
  --yellow:        #e8c547;
  --yellow-bg:     rgba(232, 197, 71, 0.14);
  --green:         #67d68c;
  --green-bg:      rgba(103, 214, 140, 0.14);
  --blue:          #5fa8ff;
  --blue-bg:       rgba(95, 168, 255, 0.14);
  --amber:         #f0a947;
  --amber-bg:      rgba(240, 169, 71, 0.14);

  --radius:        12px;
  --radius-sm:     8px;
  --gap:           16px;

  /* Tracking for small-caps section labels (.section-heading, dt cells,
     recipient-label, etc.). Single token so 10–11.5px caps read at the
     same rhythm regardless of where they appear. Tightened from 0.06em
     to 0.04em so the labels feel less stretched without losing the
     "small caps" readability. Body text and pills use 0–0.02em locally;
     very small caps (.card-score-tag at 9px) opt out and use a wider
     value inline. */
  --tracking-caps: 0.04em;
}

* { box-sizing: border-box; }

/* Skip-to-content link (WCAG 2.4.1 Bypass Blocks). Off-screen until it
   receives keyboard focus, then it slides into the top-left so a keyboard
   / screen-reader user can jump past the repeated sidebar nav straight to
   <main id="main">. Mouse users never see it. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 2000;
  padding: 10px 16px;
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  text-decoration: none;
  font-size: 13px;
  font-weight: 500;
}

.skip-link:focus {
  left: 8px;
  top: 8px;
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}

/* <main> takes tabindex=-1 so the skip link can move focus to it; never
   show a focus ring on the container itself when it receives that focus. */
.main:focus {
  outline: none;
}

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI",
               Roboto, "Helvetica Neue", Arial, sans-serif;
  font-size: 14px;
  line-height: 1.5;
  letter-spacing: 0;
  -webkit-font-smoothing: antialiased;
}

/* Form controls inherit family/weight via `font: inherit` further down, but
   the `font` shorthand does not propagate letter-spacing. Pin tracking to 0
   on every interactive element so buttons, inputs, and textarea content
   read as ordinary prose — the wide tracking is reserved for small-caps
   section labels (.section-heading, .meta-cell dt, etc.). */
button,
input,
textarea,
select {
  letter-spacing: 0;
}

input::placeholder,
textarea::placeholder {
  color: var(--text-3);
  opacity: 1;
}

body {
  display: flex;
}

/* ── Sidebar ──────────────────────────────────────────────────────────── */
/*
   Width is intentionally compact: the nav has only 2 routes + a snoozed
   substat, so anything wider wastes horizontal real estate that the board
   benefits from. The 3px left accent on `.nav-item.active` matches the
   listing-card .selected pattern so "what's selected" reads consistently
   across the app. Counts render as small pills (`.nav-pill`) inline next
   to the route they belong to, not as a loose stats block. */

.sidebar {
  width: 168px;
  min-width: 168px;
  height: 100vh;
  background: var(--sidebar);
  border-right: 1px solid var(--border);
  padding: 13px 0 11px;
  position: sticky;
  top: 0;
  display: flex;
  flex-direction: column;
}

.brand {
  padding: 0 15px 11px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 8px;
}

.brand-name {
  font-size: 15.5px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text);
}

.brand-sub {
  font-size: 10.5px;
  color: var(--text-3);
  margin-top: 2px;
}

/* Sidebar footer holds the sign-in / signed-in-as control. margin-top: auto
   pins it to the bottom of the (flex column) sidebar so it sits where users
   expect account state. Mirrors .brand's padding rhythm but with a top
   border instead of bottom — the visual seam ends the nav. */
.sidebar-footer {
  margin-top: auto;
  padding: 10px 15px 4px;
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.sidebar-user-name {
  font-size: 12px;
  color: var(--text-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Sidebar action targets both <a> (Sign in) and <button> (Sign out, inside a
   POST form for CSRF safety). Button reset (background/border/padding) + the
   anchor styling makes the form-button visually identical to the link, so
   the POST switch isn't user-visible. */
/* Sign out — <button> inside sidebar footer form, low-emphasis ghost */
.sidebar-user-action {
  font-family: inherit;
  cursor: pointer;
  text-decoration: none;
  display: inline;
  font-size: 11.5px;
  /* Sign out is an action, not chrome — --text-2 keeps it legible against
     the dark sidebar (~8.8:1 over --sidebar) while staying quieter than the
     account name above it. */
  color: var(--text-2);
  background: none;
  border: 0;
  padding: 0;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  transition: color 0.14s ease;
}

.sidebar-user-action:hover {
  color: var(--text);
}

/* Sign in — sits in the nav list, styled as a subtle gray button so it
   reads as a CTA without overpowering the nav items above it. */
.nav-sign-in {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 0 4px;
  padding: 7px 12px;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-2);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  text-decoration: none;
  transition: background 0.14s ease, border-color 0.14s ease, color 0.14s ease;
}

.nav-sign-in:hover {
  background: rgba(255, 255, 255, 0.10);
  border-color: rgba(255, 255, 255, 0.22);
  color: var(--text);
}

.nav-sign-in.active {
  color: var(--text);
  background: rgba(255, 255, 255, 0.09);
}

.sidebar-user-form {
  margin: 0;
}

/* Login page wraps Sign-in / Sign-out in <form method="post"> so OAuth
   initiation and logout both carry a CSRF token. Reset the form margin
   and let buttons sit inline alongside their sibling <a class="btn"> as
   if they were anchors themselves. */
.auth-form,
.auth-inline-form {
  margin: 0;
  display: inline;
}

.auth-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
}

.sidebar-user-action:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}

.sidebar-user-action.active {
  color: var(--text);
}

.nav {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.nav-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 15px 8px 12px;
  min-height: 34px;
  color: var(--text-2);
  text-decoration: none;
  font-size: 13px;
  font-weight: 500;
  border-left: 3px solid transparent;
  transition: color 0.14s ease, border-color 0.14s ease, background 0.14s ease;
}

.nav-item:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.04);
}

.nav-item:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: -2px;
}

.nav-item.active {
  color: var(--text);
  background: rgba(255, 255, 255, 0.06);
  border-left-color: var(--accent);
}

/* Count badge inline at the right edge of a nav item. Active state inverts
   to the accent fill so the eye reads the row + count as one selected unit;
   resting state is a muted chip that doesn't compete with the route label. */
.nav-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 20px;
  padding: 0 7px;
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  background: rgba(255, 255, 255, 0.08);
  color: var(--text-2);
  border-radius: 999px;
  letter-spacing: 0;
}

.nav-item.active .nav-pill {
  background: var(--accent);
  color: #000;
}

/* Snoozed line is a secondary stat under The Board — only renders when > 0
   (the layout suppresses it otherwise). Indented under the nav item with a
   small leading dot so it reads as "extra info about The Board" rather than
   as a primary route. Clickable: links into ?status=snoozed. */
.nav-substat {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 2px 0 4px;
  padding: 4px 15px 4px 20px;
  font-size: 11.5px;
  color: var(--text-3);
  text-decoration: none;
  transition: color 0.12s;
}

.nav-substat:hover {
  color: var(--text-2);
}

.nav-substat:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: -2px;
  color: var(--text-2);
  border-radius: 4px;
}

.nav-substat-dot {
  width: 5px;
  height: 5px;
  background: var(--text-3);
  border-radius: 50%;
  flex: 0 0 auto;
}

/* Follow-up substat: amber dot + label so quiet leads draw the eye, vs.
   the muted-grey snoozed substat right below it (snooze = "leave me be",
   follow-up = "act on this"). Matches the amber stale-contact indicator. */
.nav-substat-followup {
  color: var(--amber);
}

.nav-substat-followup:hover,
.nav-substat-followup:focus-visible {
  color: var(--amber);
}

.nav-substat-followup .nav-substat-dot {
  background: var(--amber);
}

/* Admin-mode toggle (admins only). Sits below the Admin nav-item as
   a quiet text-pill at rest; when active it lights up so the admin
   has a glanceable signal that the listing detail panel is currently
   showing admin chrome on every row. Posts to /admin/mode which
   flips session[:admin_mode] and redirects back. */
.nav-admin-mode-form {
  margin: 0 0 8px 0;
  padding: 0;
}

.nav-admin-mode {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 2px 0 0;
  padding: 4px 15px 4px 20px;
  background: transparent;
  border: 0;
  font: inherit;
  font-size: 11.5px;
  color: var(--text-3);
  cursor: pointer;
  text-align: left;
  transition: color 0.12s;
}

.nav-admin-mode:hover {
  color: var(--text-2);
}

.nav-admin-mode:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: -2px;
  color: var(--text-2);
  border-radius: 4px;
}

.nav-admin-mode-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--text-3);
  flex: 0 0 auto;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
  transition: background 0.12s, box-shadow 0.12s;
}

.nav-admin-mode.is-active {
  color: var(--blue);
  font-weight: 500;
}

.nav-admin-mode.is-active .nav-admin-mode-dot {
  background: var(--blue);
  box-shadow: 0 0 0 2px rgba(76, 154, 255, 0.18);
}

/* ── Mobile menu toggle + backdrop ───────────────────────────────────────
   Hidden on desktop; the @media (max-width: 880px) block at the bottom of
   the file flips them on, switches the sidebar to position:fixed off-canvas,
   and toggles `body.sidebar-open` from app.js. The backdrop element is
   intentionally always in the DOM (just `hidden`) — CSS controls visibility
   so JS only has to flip the body class. */

.menu-toggle {
  display: none;
  position: fixed;
  top: 12px;
  left: 12px;
  z-index: 1001;
  width: 40px;
  height: 40px;
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--text);
  cursor: pointer;
  align-items: center;
  justify-content: center;
}

.menu-toggle:hover {
  background: rgba(255, 255, 255, 0.06);
}

.menu-toggle:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}

/* Three-stripe hamburger icon, drawn via box-shadow so we don't need extra
   markup. Animates to an X when the sidebar is open. */
.menu-toggle-icon,
.menu-toggle-icon::before,
.menu-toggle-icon::after {
  display: block;
  width: 18px;
  height: 2px;
  background: currentColor;
  border-radius: 2px;
  transition: transform 0.18s ease, opacity 0.12s ease, top 0.18s ease;
}

.menu-toggle-icon {
  position: relative;
}

.menu-toggle-icon::before,
.menu-toggle-icon::after {
  content: "";
  position: absolute;
  left: 0;
}

.menu-toggle-icon::before { top: -6px; }
.menu-toggle-icon::after  { top:  6px; }

body.sidebar-open .menu-toggle-icon {
  background: transparent;
}
body.sidebar-open .menu-toggle-icon::before {
  top: 0;
  transform: rotate(45deg);
}
body.sidebar-open .menu-toggle-icon::after {
  top: 0;
  transform: rotate(-45deg);
}

.sidebar-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 999;
  background: rgba(0, 0, 0, 0.55);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease;
}

/* ── Main column ──────────────────────────────────────────────────────── */

.main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  height: 100vh;
  overflow: hidden;
}

/* ── Top bar ──────────────────────────────────────────────────────────── */

.topbar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 16px;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}

.topbar-title {
  display: flex;
  align-items: baseline;
  gap: 8px;
}

.topbar h1 {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.topbar-sub {
  color: var(--text-2);
  font-size: 13px;
}

.topbar-tabs {
  display: flex;
  gap: 2px;
}

.topbar-filter .tab {
  font-weight: 500;
}

/* Sort dropdown: native <details>. The trigger reads as "Sort: Score ↓"
   so the chosen sort + direction is visible at rest; clicking opens a
   small menu with the four sort options. Each option links to the same
   board_path() URL the old segmented tabs used, so direction-flip-on-
   reclick still works. Built to match .topbar-filters-toggle visually so
   the two dropdown triggers read as siblings. */
.topbar-sort-panel {
  position: relative;
}
.topbar-sort-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.18);
  color: var(--text-2);
  font-size: 12.5px;
  font-weight: 500;
  cursor: pointer;
  list-style: none;
  user-select: none;
  white-space: nowrap;
}
.topbar-sort-toggle::-webkit-details-marker { display: none; }
.topbar-sort-toggle::after {
  content: "▾";
  font-size: 10px;
  color: var(--text-3);
}
.topbar-sort-toggle:hover {
  border-color: rgba(120, 170, 255, 0.45);
  color: var(--text);
}
.topbar-sort-panel[open] .topbar-sort-toggle {
  border-color: rgba(120, 170, 255, 0.55);
  color: #fff;
  background: rgba(120, 170, 255, 0.10);
}
.sort-toggle-label {
  color: var(--text-3);
  font-weight: 500;
}
.topbar-sort-toggle:hover .sort-toggle-label,
.topbar-sort-panel[open] .sort-toggle-label {
  color: var(--text-2);
}
.sort-toggle-value {
  color: inherit;
  font-weight: 600;
}

.sort-menu {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 20;
  min-width: 180px;
  padding: 4px;
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.sort-menu-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 7px 10px;
  border-radius: 5px;
  color: var(--text-2);
  text-decoration: none;
  font-size: 13px;
  font-weight: 500;
}
.sort-menu-item:hover {
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
}
.sort-menu-item:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: -2px;
}
.sort-menu-item.active {
  color: var(--blue);
  background: var(--blue-bg);
}

/* Views dropdown: the derived board slices (Follow-up / Updated / Snoozed /
   Archived) that aren't pipeline statuses. Same native <details> mechanics
   and visual language as the Sort panel so the two triggers read as siblings;
   adds a per-item count badge and an active-state trigger ("Views · Updated"
   in blue) when a slice is the current view. */
.topbar-views-panel {
  position: relative;
}
.topbar-views-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.18);
  color: var(--text-2);
  font-size: 12.5px;
  font-weight: 500;
  cursor: pointer;
  list-style: none;
  user-select: none;
  white-space: nowrap;
}
.topbar-views-toggle::-webkit-details-marker { display: none; }
.topbar-views-toggle::after {
  content: "▾";
  font-size: 10px;
  color: var(--text-3);
}
.topbar-views-toggle:hover {
  border-color: rgba(120, 170, 255, 0.45);
  color: var(--text);
}
.topbar-views-panel[open] .topbar-views-toggle {
  border-color: rgba(120, 170, 255, 0.55);
  color: #fff;
  background: rgba(120, 170, 255, 0.10);
}
.topbar-views-toggle.views-on {
  border-color: rgba(120, 170, 255, 0.55);
  color: #fff;
  background: var(--blue-bg);
}
.views-toggle-label {
  color: var(--text-3);
  font-weight: 500;
}
.topbar-views-toggle:hover .views-toggle-label,
.topbar-views-panel[open] .views-toggle-label,
.topbar-views-toggle.views-on .views-toggle-label {
  color: var(--text-2);
}
.views-toggle-sep { color: var(--text-3); }
.views-toggle-value {
  color: inherit;
  font-weight: 600;
}
.views-menu {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 20;
  min-width: 260px;
  padding: 4px;
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
  display: flex;
  flex-direction: column;
  gap: 1px;
}
/* Explicit "turn the active view off" row at the top of the menu. Separated by
   a rule so it reads as an action, not a fifth view to choose from. */
.views-menu-clear {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 7px 10px;
  margin-bottom: 3px;
  border-bottom: 1px solid var(--border);
  border-radius: 5px 5px 0 0;
  color: var(--text-3);
  text-decoration: none;
  font-size: 12.5px;
}
.views-menu-clear:hover {
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
}
.views-menu-clear-x {
  font-size: 14px;
  line-height: 1;
}
.views-menu-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 7px 10px;
  border-radius: 5px;
  color: var(--text-2);
  text-decoration: none;
  font-size: 13px;
  font-weight: 500;
}
/* Stacked label + one-line "what this slice is" hint. */
.views-menu-item-main {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.views-menu-item-label {
  font-weight: 600;
}
.views-menu-item-hint {
  color: var(--text-3);
  font-size: 11px;
  font-weight: 400;
}
.views-menu-item:hover .views-menu-item-hint,
.views-menu-item.active .views-menu-item-hint {
  color: var(--text-2);
}
.views-menu-item:hover {
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
}
.views-menu-item:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: -2px;
}
.views-menu-item.active {
  color: var(--blue);
  background: var(--blue-bg);
}
.views-menu-count {
  min-width: 18px;
  height: 18px;
  padding: 0 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.10);
  color: var(--text-2);
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.views-menu-item.active .views-menu-count {
  background: var(--blue);
  color: #fff;
}

/* Filters dropdown: native <details> for show/hide (no JS). Anchored
   inside the topbar; the panel opens absolutely below the toggle so it
   doesn't push the topbar height around. */
.topbar-filters-panel {
  position: relative;
}
.topbar-filters-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.18);
  color: var(--text-2);
  font-size: 12.5px;
  font-weight: 500;
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.topbar-filters-toggle::-webkit-details-marker { display: none; }
.topbar-filters-toggle::after {
  content: "▾";
  font-size: 10px;
  color: var(--text-3);
}
.topbar-filters-toggle:hover {
  border-color: rgba(120, 170, 255, 0.45);
  color: var(--text);
}
.topbar-filters-toggle.filters-on {
  border-color: rgba(120, 170, 255, 0.55);
  color: #fff;
  background: rgba(120, 170, 255, 0.12);
}
.filters-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #4a90ff;
  display: inline-block;
}

/* Card/List view toggle: a two-button segmented control styled to read
   as a sibling of the Sort + Filters dropdown triggers (same height,
   border, resting fill). The active segment takes the blue-tinted
   treatment those triggers use when open, so "which mode am I in" reads
   the same way "which dropdown is open" does. */
.view-toggle {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.18);
  overflow: hidden;
}
.view-toggle-btn {
  border: none;
  background: transparent;
  color: var(--text-3);
  font-size: 12.5px;
  font-weight: 500;
  padding: 6px 11px;
  cursor: pointer;
  white-space: nowrap;
}
.view-toggle-btn + .view-toggle-btn {
  border-left: 1px solid var(--border);
}
.view-toggle-btn:hover {
  color: var(--text);
}
.view-toggle-btn.active {
  color: #fff;
  background: rgba(120, 170, 255, 0.12);
  font-weight: 600;
}
.view-toggle-btn:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: -2px;
}

.filters-form {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 20;
  width: min(540px, 90vw);
  padding: 16px;
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.filters-fieldset {
  margin: 0;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 6px;
}
.filters-fieldset legend {
  padding: 0 6px;
  color: var(--text-3);
  font-size: 11px;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
}
.filters-checks {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 14px;
}
.filters-check {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--text);
  text-transform: capitalize;
  cursor: pointer;
}
.filters-row {
  display: grid;
  grid-template-columns: 90px 1fr 1fr;
  gap: 10px 12px;
  align-items: end;
}
.filters-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.filters-field-grow {
  grid-column: 1 / -1;
}
.filters-label {
  font-size: 11px;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--text-3);
}
.filters-field select,
.filters-field input[type="date"],
.filters-field input[type="text"] {
  padding: 8px 10px;
  background: rgba(0, 0, 0, 0.25);
  border: 1px solid var(--input-border);
  border-radius: 6px;
  color: var(--text);
  font-family: inherit;
  font-size: 13px;
}
.filters-field select:focus,
.filters-field input:focus {
  outline: 2px solid rgba(120, 170, 255, 0.45);
  outline-offset: 1px;
  border-color: rgba(120, 170, 255, 0.6);
}
.filters-actions {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
}
@media (max-width: 880px) {
  .topbar-filters-panel {
    margin-left: 0;
    padding-left: 0;
    border-left: 0;
    width: 100%;
  }
  .filters-form {
    right: auto;
    left: 0;
    width: 100%;
  }
  .filters-row {
    grid-template-columns: 1fr 1fr;
  }
  .filters-field-grow {
    grid-column: 1 / -1;
  }
}

.tab {
  padding: 6px 12px;
  color: var(--text-2);
  text-decoration: none;
  font-size: 12px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  transition: color 0.14s ease, border-color 0.14s ease, background 0.14s ease;
}

.tab:hover {
  color: var(--text);
  border-color: var(--border);
  background: rgba(255, 255, 255, 0.04);
}

.tab:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}

.tab.active {
  color: #000;
  background: var(--accent);
  border-color: var(--accent);
}

.tab.active:hover {
  color: #000;
  background: var(--accent);
  border-color: var(--accent);
}

.sort-arrow {
  font-size: 10px;
  margin-left: 2px;
  opacity: 0.85;
}

.topbar-actions {
  margin-left: auto;
  display: flex;
  gap: 8px;
  align-items: center;
}

/* ── Buttons ──────────────────────────────────────────────────────────── */
/*
   Button hierarchy (highest → lowest emphasis):
     .btn-primary  — main safe action (Run Now, Add Listing, Save Draft,
                     Save thresholds). Filled white, highest visual weight.
     .btn-accent   — solid-blue primary used on the Admin page (Save *,
                     Approve, Make admin, Restore, Run all). Same emphasis
                     tier as .btn-primary, but reads as part of the dark
                     UI's accent family — chosen over the stark white fill
                     there so a page full of admin CTAs doesn't feel
                     detached from the surface. Distinct from the tinted
                     .btn-action-draft (a translucent CTA inside the detail
                     panel) — this one is a solid fill.
     .btn-action-draft — primary CTA *inside* the detail panel's status row.
                     Filled blue tint so it stands out from the outline
                     status pills around it. Lives on its own — see the
                     .btn-action-* block further down.
     .btn-send     — high-commitment irreversible action (Send Email).
                     Outline green, deliberately distinct from .btn-primary
                     so a misclick doesn't read as the "default" action.
                     Uses margin-left:auto inside .outreach-actions so it
                     visually separates from Save Draft.
     .btn (default) / .btn-secondary  — normal workflow actions
                     (status changes, Refresh scores, Cancel, +New).
                     Outline style, readable but lower emphasis.
     .btn-ghost    — minimum emphasis (Copy, Save note, snooze quick-picks).
                     Same visual weight as .btn but explicit muted text.
     .btn-danger   — destructive actions (Dismiss-when-it-needs-extra-
                     emphasis, currently used on the bulk Dismiss). Subtle
                     red at rest, fills on hover.
   All variants share base padding/radius/transition from .btn and inherit
   :focus-visible and :active states from it. */

.btn {
  font: inherit;
  font-weight: 500;
  letter-spacing: 0;
  border-radius: var(--radius-sm);
  padding: 7px 14px;
  cursor: pointer;
  border: 1px solid var(--border);
  background: var(--surface-1);
  color: var(--text);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  transition: background 0.14s ease, border-color 0.14s ease,
              color 0.14s ease, box-shadow 0.16s ease,
              transform 0.08s ease;
}

.btn:hover {
  border-color: var(--border-strong);
  background: var(--surface-2);
  transform: translateY(-1px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.22);
}

.btn:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}

.btn:active {
  transform: translateY(1px);
  box-shadow: none;
  transition-duration: 0.06s;
}

/* Disabled: clearly inert (dimmed + not-allowed cursor, no hover lift) but
   the label stays readable rather than fading to illegible. Covers every
   variant via the base class. */
.btn:disabled,
.btn[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
  box-shadow: none;
  transform: none;
}
.btn:disabled:hover,
.btn[disabled]:hover {
  transform: none;
  box-shadow: none;
  background: var(--surface-1);
  border-color: var(--border);
}

.btn-secondary {
  /* Explicit alias of .btn for templates that want to be deliberate. */
}

.btn-primary {
  background: var(--accent);
  color: #000;
  border-color: var(--accent);
  font-weight: 600;
}

.btn-primary:hover {
  background: #f4f4f4;
  border-color: #f4f4f4;
  box-shadow: 0 2px 14px rgba(255, 255, 255, 0.18);
}

.btn-primary:focus-visible {
  outline-color: var(--accent);
}

/* Accent: solid-blue primary. Same emphasis tier as .btn-primary but tied
   to the --blue accent the dark UI already uses (subnav active pill,
   .admin-pill-self, selected-row left bar). Dark navy text on the bright
   blue clears WCAG AA — the exact pairing .admin-pill-self already ships.
   Used for the Admin page's primary + Save actions so they read as part of
   the surface rather than as detached white chips. */
.btn-accent {
  background: var(--blue);
  color: #06243f;
  border-color: var(--blue);
  font-weight: 600;
}

.btn-accent:hover {
  background: #7bb8ff;
  border-color: #7bb8ff;
  box-shadow: 0 2px 14px rgba(95, 168, 255, 0.28);
}

.btn-accent:focus-visible {
  outline-color: var(--blue);
}

/* Ghost: muted text, no border emphasis. Used for low-priority actions
   (Copy, Save note, snooze quick-picks) where the action is fine but
   shouldn't compete with the primary CTA. */
.btn-ghost {
  background: transparent;
  color: var(--text-2);
  border-color: var(--border);
}

.btn-ghost:hover {
  color: var(--text);
  border-color: var(--border-strong);
  background: rgba(255, 255, 255, 0.03);
}

/* Danger: destructive. Subtle red border + red text at rest, fills to
   red-tinted background on hover. Not screaming — readable as "this is
   the destructive option" without dominating the row. */
.btn-danger {
  background: transparent;
  color: var(--red);
  border-color: rgba(255, 107, 107, 0.35);
}

.btn-danger:hover {
  background: var(--red-bg);
  border-color: var(--red);
  color: var(--red);
}

.btn-danger:focus-visible {
  outline-color: var(--red);
}

/* Send: high-commitment, irreversible. Outline green so it reads as
   deliberate ("this triggers the actual email") and clearly distinct
   from the white .btn-primary on the same row. Margin-left:auto inside
   .outreach-actions pushes it to the far right of the row, separating
   it from Save Draft. */
.btn-send {
  background: transparent;
  color: var(--green);
  border-color: rgba(103, 214, 140, 0.45);
  font-weight: 600;
  padding: 7px 18px;
}

/* Send is high-commitment: no lift, no shadow on hover. The color/border
   shift is the entire feedback so it stays deliberate against the lifted
   buttons next to it (Save Draft, Copy). */
.btn-send:hover {
  background: var(--green-bg);
  border-color: var(--green);
  transform: none;
  box-shadow: none;
}

.btn-send:focus-visible {
  outline-color: var(--green);
}

/* ── Board (two-panel layout) ─────────────────────────────────────────── */

.board {
  flex: 1;
  min-height: 0;
  display: flex;
  gap: 14px;
  padding: 14px 16px 16px;
  overflow: hidden;
}

/* Both panels are intentional surfaces. Listings panel is the "index" — a
   slightly darker well so it reads as subordinate to the brighter detail
   panel where reading + writing happens. Border-strong gives both panels a
   clearer edge against the body bg without being noisy. */
.panel {
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  overflow-y: auto;
}

.listings-panel {
  width: 380px;
  min-width: 340px;
  flex: 0 0 380px;
  background: #242424;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.listings-header {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
}

.select-all-label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 11.5px;
  color: var(--text-3);
  cursor: pointer;
  user-select: none;
}

.select-all-label:hover { color: var(--text-2); }

/* Helper text on the listings header that disambiguates the "Select all"
   checkbox — without it the checkbox reads as ornament. Hidden once the
   user has selected at least one row (body.bulk-active toggles in app.js)
   so the count in the bottom bar takes over as the live affordance. */
.bulk-hint {
  font-size: 11px;
  color: var(--text-3);
  letter-spacing: 0.02em;
  font-style: italic;
}

body.bulk-active .bulk-hint {
  visibility: hidden;
}

/* Match the per-card .listing-card-checkbox sizing (16x16, accent-color)
   so the toolbar control reads as the same affordance, just always visible. */
.select-all-label input[type="checkbox"] {
  width: 16px;
  height: 16px;
  margin: 0;
  cursor: pointer;
  accent-color: var(--accent);
}

.listings-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 6px 6px 8px;
}

/* Sticky bulk action bar at the bottom of the listings panel. The panel
   itself is a flex column with overflow:hidden; this sits as the last
   flex child so it stays pinned while the cards scroll above it. The
   blue-tinted fill + accent left bar mirror the selected-card treatment
   so the bar reads as the same affordance — "you have an active
   selection" — pinned to the bottom of the panel. */
.bulk-action-bar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  border-top: 1px solid var(--border-strong);
  border-left: 3px solid var(--blue);
  background: linear-gradient(to right, rgba(95, 168, 255, 0.10), rgba(255, 255, 255, 0.04) 40%);
}

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

.bulk-count {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  letter-spacing: 0.01em;
}

.bulk-action-buttons {
  display: flex;
  gap: 6px;
  margin-left: auto;
  flex-wrap: wrap;
}

.bulk-clear {
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-3);
  cursor: pointer;
  font-size: 18px;
  line-height: 1;
  padding: 2px 8px;
  border-radius: var(--radius-sm);
}

.bulk-clear:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.06);
}

/* Detail panel keeps zero padding so the inner .detail can carry its own
   capped, centered content column. Content padding lives on .detail; the
   panel's overflow-y:auto is what scrolls (.detail-header sticks to its top). */
.detail-panel {
  flex: 1;
  min-width: 0;
  padding: 0;
}

/* "← Board" link rendered in every detail panel but hidden on desktop —
   it only earns its place at phone widths, where the list and detail are
   full-screen siblings rather than side-by-side panels. The mobile block
   at the bottom flips it on and makes it a sticky top bar. */
.detail-back {
  display: none;
}

.settings-panel {
  flex: 1;
  min-width: 0;
  padding: 28px 32px;
  max-width: 720px;
}

.settings-content {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.settings-hint {
  color: var(--text-2);
  font-size: 13px;
  line-height: 1.55;
  margin: 0 0 14px;
  max-width: 56ch;
}

.empty {
  padding: 40px 20px;
  text-align: center;
  color: var(--text-3);
  font-size: 13px;
}

/* ── Contextual board empty state ─────────────────────────────────────── */
/* Replaces the one-line "No listings to show." with a titled explanation of
   WHY this particular view is empty (see dashboard/board_empty_state.rb).
   Constrained width so the body copy wraps to a readable measure rather than
   spanning a wide list panel. */
.board-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  max-width: 380px;
  margin: 0 auto;
}
.board-empty-title {
  margin: 0;
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
}
.board-empty-body {
  margin: 0;
  color: var(--text-3);
  font-size: 12.5px;
  line-height: 1.5;
}
.board-empty .btn {
  margin-top: 4px;
}

/* ── Board state bar ──────────────────────────────────────────────────── */
/* Sits between the topbar and the board, shown whenever the board is off its
   default state. Left: what's narrowing the board, as removable chips (the
   active VIEW included — that's the visible way to de-select it). Right: the
   Reset that clears stage + view + filters + sort together. Wraps on narrow
   widths; the reset stays on the trailing edge on wide ones. */
.board-state-bar {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 16px 0;
}
.board-state-chips {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
.board-state-label {
  color: var(--text-3);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-right: 2px;
}
/* The reset is the escape hatch, so it reads as an action rather than a chip —
   but stays quiet enough not to compete with the topbar's primary buttons. */
.board-state-reset {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  color: var(--text-2);
  background: transparent;
  font-size: 12px;
  text-decoration: none;
  white-space: nowrap;
}
.board-state-reset:hover {
  border-color: var(--blue);
  color: var(--text);
}
.filter-chips-label {
  color: var(--text-3);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-right: 2px;
}
.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 8px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--panel);
  color: var(--text-2);
  font-size: 12px;
  text-decoration: none;
  max-width: 260px;
}
.filter-chip:hover {
  border-color: var(--blue);
  color: var(--text);
}
.filter-chip-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.filter-chip-x {
  color: var(--text-3);
  font-size: 13px;
  line-height: 1;
}
.filter-chip:hover .filter-chip-x {
  color: var(--blue);
}
/* A View is a different KIND of narrowing than a Filter (cross-cutting slice
   vs. a field match), so its chip carries the same blue accent the Views
   trigger + Updated pill use, keeping the two visually separable in one row. */
.filter-chip-view {
  border-color: rgba(95, 168, 255, 0.45);
  color: var(--blue);
}
.filter-chip-view .filter-chip-x {
  color: var(--blue);
}
.filter-chip-view:hover {
  border-color: var(--blue);
  background: rgba(95, 168, 255, 0.10);
  color: var(--blue);
}
.filter-chips-clear {
  color: var(--text-3);
  font-size: 11.5px;
  text-decoration: underline;
  margin-left: 2px;
}
.filter-chips-clear:hover {
  color: var(--text);
}

/* ── Board pagination ─────────────────────────────────────────────────── */
/* Pinned footer of .listings-panel (a flex column): flex 0 0 auto keeps it
   below the scrolling card list and always on screen, mirroring how the
   bulk-action bar pins to the same panel. */
.board-pager {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 10px 12px;
  font-size: 13px;
  border-top: 1px solid var(--border);
  background: #242424;
}

.board-pager-status {
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}

.board-pager-btn {
  padding: 6px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text-2);
  text-decoration: none;
  white-space: nowrap;
}

.board-pager-btn:hover {
  border-color: var(--border-strong);
  color: var(--text);
}

.board-pager-btn.is-disabled {
  opacity: 0.4;
  pointer-events: none;
}

/* ── Listing cards ────────────────────────────────────────────────────── */

.listing-card {
  display: block;
  text-decoration: none;
  color: inherit;
  padding: 12px 14px 10px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  border-left: 3px solid transparent;
  margin-bottom: 6px;
  /* cubic-bezier(0.16, 1, 0.3, 1) is an ease-out-expo curve — starts fast,
     decelerates smoothly. Gives the hover lift a more polished feel than
     a plain ease without crossing into "animated marketing site" territory. */
  transition: background 0.16s cubic-bezier(0.16, 1, 0.3, 1),
              border-color 0.16s cubic-bezier(0.16, 1, 0.3, 1),
              box-shadow 0.18s cubic-bezier(0.16, 1, 0.3, 1),
              transform 0.16s cubic-bezier(0.16, 1, 0.3, 1);
  position: relative;
}

/* Stretched anchor: the whole card is clickable, but we can't wrap the card
   in an <a> because it now contains a real <a> for the source link (nested
   anchors auto-close in the parser). Instead, this empty anchor sits
   absolutely over the card. The source link uses position:relative + z-index
   to sit above this and capture its own clicks. */
.listing-card-link {
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
}

.listing-card-link:focus-visible {
  /* Blue ring distinguishes keyboard focus from the selected state's
     white left-accent. A user tabbing through the list past the
     currently-selected card can clearly tell which card is *focused*
     vs which is *selected*. */
  outline: 2px solid var(--blue);
  outline-offset: -2px;
}

/* Bulk-select checkbox: absolutely positioned over the top-left of each
   card so it doesn't shift any existing content. Hidden by default;
   reveals on card hover, when itself checked, or when any other card on
   the board is checked (body.bulk-active is toggled by app.js).
   z-index 2 keeps it above the stretched .listing-card-link overlay so
   clicks land on the checkbox, not the link. */
.listing-card-checkbox {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 2;
  width: 16px;
  height: 16px;
  margin: 0;
  cursor: pointer;
  opacity: 0;
  accent-color: var(--accent);
  transition: opacity 0.12s;
}

.listing-card:hover .listing-card-checkbox,
.listing-card-checkbox:checked,
.listing-card-checkbox:focus-visible,
body.bulk-active .listing-card-checkbox {
  opacity: 1;
}

.listing-card-checkbox:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.card-source-link {
  position: relative;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  color: var(--text-3);
  text-decoration: none;
  font-size: 13px;
  line-height: 1;
  border-radius: 4px;
  transition: color 0.12s, background 0.12s;
}

.card-source-link:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.06);
}

.card-source-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  color: var(--text);
}

/* Single-source variant that shows a text label (e.g. "Discord Monitor ↗")
   instead of the bare arrow. The base rule above is a fixed 20×20 icon box
   sized for the lone ↗ — a full label overflows it and spills onto the
   neighbouring status pill / ★ Ratings link. Relax the box to hug its
   content (same treatment the multi-source list uses) and keep the label
   on one line so the arrow never wraps away from its text. */
.card-source-link.is-labeled {
  width: auto;
  height: auto;
  padding: 0 4px;
  font-size: inherit;
  white-space: nowrap;
}

/* Multi-source domain list (rendered when a listing has 2+ source
   entries). The single-source ↗ icon path above keeps its dedicated
   rules; selectors below scope only to anchors INSIDE the list, so
   the icon path's fixed 20×20 box doesn't leak onto text links. */
.card-source-list {
  color: var(--text-3);
}

.card-source-list .card-source-link {
  width: auto;
  height: auto;
  font-size: inherit;
  color: inherit;
  background: transparent;
  text-decoration: none;
}

.card-source-list .card-source-link:hover {
  color: var(--text);
  background: transparent;
  text-decoration: underline;
}

.card-source-sep {
  color: var(--text-3);
  padding: 0 2px;
}

/* Trailing "+N" counter when a listing has more deduped source domains
   than fit on the card meta row. Muted to match .card-source-link's
   resting style — it's a count, not a link, so no underline-on-hover. */
.card-source-more {
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  cursor: help;
}

.listing-card:hover {
  background: rgba(255, 255, 255, 0.055);
  border-color: var(--border);
  border-left-color: var(--border-strong);
  transform: translateY(-1px);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25);
}

/* Keyboard focus mirrors the hover lift so Tab users get the same tactile
   signal as mouse users. The blue ring on .listing-card-link below is the
   primary focus indicator; the lift is a secondary "you're on this row"
   cue. :focus-within is intentionally chosen over :has() — it triggers
   for any focusable descendant (link, bulk-select checkbox), which is the
   right behavior for keyboard navigation through the board. */
.listing-card:focus-within {
  background: rgba(255, 255, 255, 0.055);
  transform: translateY(-1px);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25);
}

/* Selected state — three reinforcing signals so the active card is
   unmistakable without being loud:
   1. White accent bar on the left edge (3px, contrasts the muted-grey
      hover border so a hovered card can't be confused with the active one).
   2. Stronger background wash + subtle inset border that lifts the card
      off the panel.
   3. Brighter headline text. */
.listing-card.selected {
  background: rgba(255, 255, 255, 0.07);
  border-color: var(--border-strong);
  border-left-color: var(--accent);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04),
              0 4px 14px rgba(0, 0, 0, 0.28);
}

.listing-card.selected .card-headline {
  color: #fff;
}

/* Cards stuck in `contacted` past STALE_CONTACT_DAYS get a muted amber wash
   on the left edge so dropped follow-ups read at a glance. Selection still
   wins for the left border so the active row pops against the rest. */
.listing-card.stale {
  border-left-color: var(--amber);
  background: linear-gradient(to right, var(--amber-bg), transparent 55%);
}

.listing-card.stale:hover {
  background: linear-gradient(to right, var(--amber-bg), rgba(255, 255, 255, 0.03) 55%);
}

.listing-card.stale.selected {
  border-left-color: var(--accent);
  background: rgba(255, 255, 255, 0.05);
}

.stale-flag {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: var(--amber);
  background: var(--amber-bg);
  border-radius: 999px;
  padding: 2px 8px;
  font-size: 10.5px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}

.stale-flag-inline {
  margin-left: 8px;
  font-size: 11.5px;
}

.stale-icon {
  font-size: 11px;
  line-height: 1;
}

/* "Updated Xh ago" pill — a listing got genuinely new shared info (a merge
   overwrite or a Discord reply enrichment) within UPDATED_WINDOW_HOURS. Blue
   "fresh info" accent, deliberately distinct from the amber stale-flag so the
   two read as different signals at a glance. */
.updated-pill {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: var(--blue);
  background: var(--blue-bg);
  border-radius: 999px;
  padding: 2px 8px;
  font-size: 10.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.updated-pill-inline {
  margin-left: 8px;
  font-size: 11.5px;
}
.updated-icon {
  font-size: 10px;
  line-height: 1;
}

.listing-card-top {
  display: flex;
  gap: 12px;
  margin-bottom: 10px;
}

.listing-card-text {
  flex: 1;
  min-width: 0;
}

/* Chevron sits at the right edge of the title row as a visible "open
   detail" affordance — the rest of the card is also clickable via the
   stretched .listing-card-link overlay, but the chevron makes
   clickability legible at rest. Visible at low opacity always; bumps
   to full opacity on hover/selected so the active card reads as the
   "destination" and hovered rows preview the move. pointer-events:none
   so it doesn't intercept clicks meant for the stretched link. */
.listing-card-chevron {
  flex: 0 0 auto;
  align-self: flex-start;
  margin-top: -2px;
  margin-left: 2px;
  font-size: 18px;
  line-height: 1;
  color: var(--text-3);
  opacity: 0.5;
  transition: opacity 0.12s, color 0.12s, transform 0.12s;
  pointer-events: none;
}

.listing-card:hover .listing-card-chevron {
  opacity: 1;
  color: var(--text-2);
  transform: translateX(2px);
}

.listing-card.selected .listing-card-chevron {
  opacity: 1;
  color: var(--text);
}

.card-headline {
  font-weight: 600;
  color: var(--text);
  font-size: 14px;
  line-height: 1.35;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  word-break: break-word;
}

.card-subhead {
  font-size: 12px;
  color: var(--text-2);
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.card-show-title {
  font-size: 11px;
  font-style: italic;
  color: var(--text-3);
  margin-top: 3px;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.promoter {
  font-size: 11px;
  color: var(--text-3);
  margin-top: 2px;
}

.listing-card-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11.5px;
  color: var(--text-2);
  flex-wrap: wrap;
}

/* Trailing group — status pill + external-source link sit together on the
   far right so the status reads as the "what's its current state" anchor
   for the row, with the source-link as a related affordance next to it. */
.meta-trail {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
}

.meta-date {
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
}

.card-scores {
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex: 0 0 auto;
}

/* Score badge: small label ("Book"/"Worth") + numeric value. The label is
   uppercased mini-cap with tracking so it reads as a tag against the
   value — the pattern lets the eye lock onto the number while the word
   disambiguates which dimension. min-width sized to the longer "Worth"
   label + 3-digit value so both badges align as a column. */
.card-score {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  min-width: 70px;
  height: 22px;
  padding: 0 8px;
  font-size: 11.5px;
  font-weight: 600;
  border-radius: var(--radius-sm);
  font-variant-numeric: tabular-nums;
  transition: background-color 0.18s ease, color 0.18s ease;
}

.card-score-tag {
  font-size: 9px;
  font-weight: 700;
  opacity: 0.78;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.card-score-value {
  font-weight: 700;
  letter-spacing: 0;
}

/* ── Score color bands ────────────────────────────────────────────────────
   Applied to .card-score badges on the board, the .score-card-value chip
   inside the AI assessment, or any other element that wants the standard
   green/amber/red treatment via score_class(value). */

.score-green,
.card-score.score-green { background: var(--green-bg); color: var(--green); }
.score-amber,
.card-score.score-amber { background: var(--amber-bg); color: var(--amber); }
.score-red,
.card-score.score-red   { background: var(--red-bg);   color: var(--red); }
.score-none,
.card-score.score-none  { background: rgba(255,255,255,0.04); color: var(--text-3); }

/* ── Board list view ──────────────────────────────────────────────────────
   Alternate rendering mode for the listings panel (?view=list /
   board_defaults.view). One column-aligned grid row per listing instead
   of a card. The panel deliberately widens — the card view's fixed
   380px is load-bearing for cards, but a single scannable row needs the
   width, so list mode trades detail-panel width for column alignment.
   Both .list-cols (the caption row) and .listing-row share one
   grid-template so captions and cells stay aligned; change one and you
   must change the other (including the responsive overrides below). */
/* The listings panel is a size container in list mode so the column-
   shedding @container rules below key off the panel's ACTUAL width —
   which moves with the drag-resizer and the collapse toggle, not just
   the viewport. Applies on mobile too (panel = full width there). */
.board.board-view-list .listings-panel {
  container-type: inline-size;
}

/* Desktop-only: at ≤880px the board stacks vertically and the generic
   .listings-panel { width:100%; flex:0 0 auto } stacked rules must win —
   these selectors are more specific than those, so without the min-width
   guard they'd force a row split onto a column layout.

   --board-split is set inline on .board by app.js (CSSOM, CSP-safe) from
   the drag-resizer / localStorage; 56% is the resting default. The
   listings panel takes the split as a non-growing basis and the detail
   panel absorbs the remainder, so one number drives the whole split. */
@media (min-width: 881px) {
  .board.board-view-list {
    gap: 4px;
  }
  .board.board-view-list .listings-panel {
    width: auto;
    min-width: 0;
    flex: 0 1 var(--board-split, 56%);
  }
  .board.board-view-list .detail-panel {
    flex: 1 1 0;
    min-width: 0;
  }
  .board.board-view-list .board-resizer {
    display: flex;
  }
  .board.board-view-list.detail-collapsed .listings-panel {
    flex: 1 1 100%;
  }
  .board.board-view-list.detail-collapsed .detail-panel,
  .board.board-view-list.detail-collapsed .board-resizer {
    display: none;
  }
}

/* Drag handle between the panels (list view, desktop only — the
   media query above reveals it). The grip line brightens + grows on
   hover/drag so the affordance is discoverable without reading docs. */
.board-resizer {
  display: none;
  align-items: center;
  justify-content: center;
  flex: 0 0 10px;
  cursor: col-resize;
  border-radius: 4px;
  touch-action: none;
}
.board-resizer::before {
  content: "";
  width: 3px;
  height: 48px;
  border-radius: 2px;
  background: var(--border-strong);
  transition: background 0.12s, height 0.12s;
}
.board-resizer:hover::before,
body.board-resizing .board-resizer::before {
  background: rgba(120, 170, 255, 0.6);
  height: 72px;
}
.board-resizer:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: -3px;
}
body.board-resizing {
  user-select: none;
  cursor: col-resize;
}

/* Hide/Show details toggle in the listings header (list view only).
   Quiet text button — it's a layout preference, not a workflow action. */
.detail-collapse-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex: 0 0 auto;
  border: none;
  background: transparent;
  color: var(--text-3);
  font-size: 11.5px;
  font-weight: 500;
  padding: 2px 4px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  white-space: nowrap;
}
.detail-collapse-btn:hover { color: var(--text); }
.detail-collapse-btn:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 1px;
}
.detail-collapse-btn .collapse-icon {
  font-size: 13px;
  line-height: 1;
}
@media (max-width: 880px) {
  /* Stacked layout — there's no side-by-side panel to collapse. */
  .detail-collapse-btn { display: none; }
}

.list-cols,
.listing-row {
  display: grid;
  grid-template-columns:
    20px                  /* bulk checkbox */
    minmax(120px, 1.5fr)  /* title / lineup */
    minmax(90px, 1fr)     /* venue */
    minmax(70px, 0.8fr)   /* promoter */
    104px                 /* date / festival span */
    64px                  /* pay */
    42px 42px             /* book / worth */
    96px                  /* status pill + stale flag */
    14px;                 /* chevron */
  gap: 0 10px;
  align-items: center;
}

/* Caption row pinned between the Select-all header and the scrolling row
   list (a flex child of .listings-panel, not inside .listings-scroll, so
   it never scrolls away). Horizontal padding mirrors the rows' own
   padding + the scroll container's 6px inset so captions sit over their
   columns. */
.list-cols {
  flex: 0 0 auto;
  padding: 6px 14px 6px 17px;
  border-bottom: 1px solid var(--border);
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-3);
}
.list-cols .list-col-score { text-align: center; }
.list-cols .list-col-pay   { text-align: right; }
.list-cols > span {
  overflow: hidden;
  white-space: nowrap;
}

.listing-row {
  position: relative;
  padding: 7px 8px 7px 11px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  border-left: 3px solid transparent;
  margin-bottom: 2px;
  transition: background 0.16s cubic-bezier(0.16, 1, 0.3, 1),
              border-color 0.16s cubic-bezier(0.16, 1, 0.3, 1);
}

.listing-row:hover,
.listing-row:focus-within {
  background: rgba(255, 255, 255, 0.055);
  border-left-color: var(--border-strong);
}

/* Same three selected signals as cards, minus the lift (rows are dense —
   a translate would jitter the column grid): white left accent, brighter
   wash, white headline. */
.listing-row.selected {
  background: rgba(255, 255, 255, 0.07);
  border-color: var(--border-strong);
  border-left-color: var(--accent);
}
.listing-row.selected .row-headline { color: #fff; }

.listing-row.stale {
  border-left-color: var(--amber);
  background: linear-gradient(to right, var(--amber-bg), transparent 35%);
}
.listing-row.stale:hover {
  background: linear-gradient(to right, var(--amber-bg), rgba(255, 255, 255, 0.03) 35%);
}
.listing-row.stale.selected {
  border-left-color: var(--accent);
  background: rgba(255, 255, 255, 0.05);
}

/* In-flow counterpart of .listing-card-checkbox: same size, accent and
   reveal rules (hover / checked / focus / any-selection), but a grid
   cell rather than an absolute overlay so the row's first column
   reserves its space and the headline never reflows when it appears. */
.listing-row-checkbox {
  position: relative;
  z-index: 2;
  width: 15px;
  height: 15px;
  margin: 0;
  display: block;
  cursor: pointer;
  opacity: 0;
  accent-color: var(--accent);
  transition: opacity 0.12s;
}
.listing-row:hover .listing-row-checkbox,
.listing-row-checkbox:checked,
.listing-row-checkbox:focus-visible,
body.bulk-active .listing-row-checkbox {
  opacity: 1;
}
.listing-row-checkbox:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.row-headline,
.row-cell {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.row-headline {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
}
.row-cell {
  font-size: 12px;
  color: var(--text-2);
}
.list-col-date { font-variant-numeric: tabular-nums; }
.list-col-pay  { text-align: right; }

/* Missing-value cells (fresh Discord ingests pre-extraction, unscored
   rows): the em-dash renders muted so absence reads as absence, not as
   a value. */
.row-empty { color: var(--text-3); }

.row-score {
  font-size: 12px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: center;
  border-radius: 5px;
  padding: 2px 0;
}

.list-col-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  overflow: hidden;
}

.listing-row .listing-card-chevron {
  margin: 0;
  align-self: center;
  font-size: 16px;
}
.listing-row:hover .listing-card-chevron {
  opacity: 1;
  color: var(--text-2);
  transform: translateX(2px);
}
.listing-row.selected .listing-card-chevron {
  opacity: 1;
  color: var(--text);
}

/* Column shedding keyed on the PANEL's width (container queries against
   .listings-panel, which is container-type: inline-size in list mode) —
   not the viewport. The panel's width moves with the drag-resizer and
   the Hide-details toggle, so viewport media queries would shed the
   wrong columns (a 1280px window with a 56% split has a ~640px panel).
   Promoter sheds first, then pay — secondary triage signals; venue /
   date / scores / status are the load-bearing scan columns. Keep each
   template in lockstep with .list-cols (the caption row sheds the same
   cells). */
@container (max-width: 959px) {
  .list-cols,
  .listing-row {
    grid-template-columns:
      20px minmax(120px, 1.5fr) minmax(90px, 1fr) 104px 64px 42px 42px 96px 14px;
  }
  .list-col-promoter { display: none; }
}

@container (max-width: 779px) {
  .list-cols,
  .listing-row {
    grid-template-columns:
      20px minmax(110px, 1.5fr) minmax(80px, 1fr) 96px 38px 38px 92px 14px;
  }
  .list-col-promoter,
  .list-col-pay { display: none; }
}

@container (max-width: 619px) {
  .list-cols,
  .listing-row {
    grid-template-columns:
      20px minmax(90px, 1.5fr) 96px 40px 40px 86px;
  }
  .list-col-promoter,
  .list-col-pay,
  .list-col-venue,
  .list-col-chevron { display: none; }
}

/* Narrow phones: headline / date / scores / status is the densest set
   that fits ~360px without horizontal scroll. Status is widened to 92px so
   "Contacted" (the common stale-follow-up state) no longer clips mid-word;
   date + scores give up the room. The longest label ("Shortlisted") still
   clips, with the status dot as the redundant colour cue. */
@container (max-width: 479px) {
  .list-cols,
  .listing-row {
    grid-template-columns:
      20px minmax(74px, 1.5fr) 56px 38px 38px 92px;
    gap: 0 6px;
  }
}

/* ── Status pills ─────────────────────────────────────────────────────── */
/*
   Dot + uppercase label, no filled pill. The colored dot (::before)
   carries the status hue; the label stays in --text-2 so contrast
   against any background is constant and the eye can scan a row of
   status indicators without competing color washes. Readable text
   labels remain the primary signal — color is a redundant cue only.
   The .status-pill .status-* classes here are also applied to the
   message-history "Sent / Failed" markers in [listing.erb], so green
   and red carry through there as well. */
.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0;
  background: transparent;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-2);
  line-height: 1.4;
  transition: color 0.18s ease;
}

.status-pill::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--text-3);
  flex: 0 0 auto;
}

.status-new::before          { background: var(--blue); }
.status-shortlisted::before  { background: var(--amber); }
.status-contacted::before    { background: var(--yellow); }
.status-booked::before       { background: var(--green); }
.status-dismissed::before    { background: var(--red); }
.status-muted::before        { background: var(--text-3); }

/* ── Pay amount coloring ──────────────────────────────────────────────── */

.pay          { font-weight: 500; }
.pay-red      { color: var(--red); }
.pay-yellow   { color: var(--yellow); }
.pay-green    { color: var(--green); }
.pay-unknown  { color: var(--text-3); }

/* ── Detail panel ─────────────────────────────────────────────────────── */

/* Inner column inside the scrollable detail panel. max-width caps long-form
   prose (rationale, description, outreach body) at a comfortable line length
   on wide laptop screens; margin:auto centers the column inside whatever
   width the panel ends up at. Padding lives here (not on .detail-panel) so
   the sticky .detail-header below can reach the panel's top edge cleanly. */
.detail {
  display: flex;
  flex-direction: column;
  gap: 18px;
  max-width: 880px;
  margin: 0 auto;
  padding: 22px 28px 28px;
}

/* Sticky so title + lineup + meta-bar stays visible while scrolling long
   rationale/outreach/description below. Background matches the panel so
   content scrolls cleanly behind it. .detail's padding-top gives initial
   breathing room above the title; once the user scrolls past it, the
   header pins to top:0 of the (.detail-panel) scroll container. */
.detail-header {
  position: sticky;
  top: 0;
  z-index: 5;
  background: var(--panel);
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border);
}

/* Headline is the strongest text element in the panel. Clamps to 2 lines
   so a title that's actually a 150-act lineup ("A, B, C +147 more")
   doesn't dominate the panel. Full text stays accessible via the
   title="" tooltip and the lineup expander below. */
.detail-headline {
  margin: 0 0 2px;
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.018em;
  line-height: 1.2;
  color: #fff;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  word-break: break-word;
}

/* Venue subhead reads as the "where" beneath the title. Lower visual
   weight than the headline; still legible at body-text size. */
.detail-subhead {
  color: var(--text-2);
  font-size: 13.5px;
  font-weight: 500;
  margin-top: 4px;
}

/* Private-listing badge (Phase 7). Renders below the title for any
   listing where visibility='private'. Yellow tint so it reads as a
   "heads up — this is special" affordance, not a hard error. The
   read-path filter that actually hides private listings from other
   users ships in Phase 2/3; until then this badge is the only
   visual indication that the row is meant to be user-scoped. */
.detail-visibility-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 6px 0 0;
  padding: 3px 10px;
  background: var(--yellow-bg);
  border: 1px solid rgba(232, 197, 71, 0.35);
  border-radius: 999px;
  font-size: 12px;
  color: var(--yellow);
  letter-spacing: 0.01em;
}

.detail-visibility-icon {
  font-size: 11px;
  line-height: 1;
}

.detail-visibility-label {
  font-weight: 500;
}

/* Creator/admin-only visibility control. Reuses the badge's pill shape
   so a non-managing viewer's read-only badge and a manager's interactive
   control read as the same artifact, just with an action attached. The
   icon swaps (🔒 private, 🌐 shared) so the current state is glanceable
   without reading the label. */
.detail-visibility-control {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 6px 0 0;
  padding: 3px 10px 3px 10px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  font-size: 12px;
  color: var(--text-muted);
  letter-spacing: 0.01em;
}

.detail-visibility-control .detail-visibility-label {
  font-weight: 500;
  color: var(--text);
}

.detail-visibility-control .btn-sm {
  margin-left: 4px;
  padding: 2px 8px;
  font-size: 11px;
}

.detail-visibility-note {
  font-size: 11px;
  color: var(--text-muted);
  font-style: italic;
}

/* Lineup line: the "subtitle" sitting beneath the headline at lower
   visual weight. Preview shows up to ~3 acts +N more; the inline
   <details> expander reveals the full comma-joined lineup beneath it
   without leaving the page. */
.detail-lineup {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 8px;
  margin-top: 4px;
  font-size: 13px;
  color: var(--text-2);
  line-height: 1.45;
}

.detail-lineup-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-3);
  font-weight: 500;
  flex: 0 0 auto;
}

.detail-lineup-preview {
  flex: 1 1 200px;
  min-width: 0;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  word-break: break-word;
}

.detail-lineup-toggle {
  flex: 1 1 100%;
  margin: 2px 0 0;
}

.detail-lineup-toggle > summary {
  cursor: pointer;
  list-style: none;
  display: inline-block;
  font-size: 11.5px;
  color: var(--blue);
  user-select: none;
}

.detail-lineup-toggle > summary::-webkit-details-marker { display: none; }

.detail-lineup-toggle > summary:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
  border-radius: 3px;
}

.detail-lineup-toggle[open] > summary {
  color: var(--text-2);
}

.detail-lineup-full {
  margin: 6px 0 0;
  padding: 10px 12px;
  background: rgba(0, 0, 0, 0.18);
  border-radius: var(--radius-sm);
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--text-2);
  word-break: break-word;
}

.detail-promoter {
  color: var(--text-3);
  font-size: 11.5px;
  margin-top: 4px;
}

/* Compact inline meta row — Date / Location / Pay / Source / Status /
   Contacted all sit on a single horizontal strip with small uppercase
   labels followed by their values. Wraps to a second line on narrow
   widths; gap-only separation (no dividers) keeps the row quiet. */
.detail-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 18px;
  margin: 0;
  padding: 0;
  line-height: 1.5;
}

.meta-cell {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  min-width: 0;
}

.meta-cell dt {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-3);
  font-weight: 500;
  margin: 0;
  flex: 0 0 auto;
}

.meta-cell dd {
  margin: 0;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  min-width: 0;
}

/* Source link reads as clickable — blue, underline-on-hover, focus ring.
   Other meta values are plain text so the link stands out as the one
   navigable element on the row. */
.meta-cell dd a {
  color: var(--blue);
  text-decoration: none;
  font-weight: 500;
}

.meta-cell dd a:hover {
  text-decoration: underline;
}

.meta-cell dd a:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
  border-radius: 3px;
}

.section-heading {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-2);
  /* margin-top:0 so the class is safe on <h3> (heading default top margin
     would otherwise shift these section labels). Used on <h2>/<h3>/<div>/
     <span>/<summary> — the reset keeps them visually identical. */
  margin: 0 0 10px;
}

/* Description is the raw scraped event copy — moved below outreach as
   "full details if needed" and collapsed by default so it doesn't
   dominate the panel. Disclosure caret matches the outreach <details>
   pattern so the two sections feel consistent. */
details.description {
  background: var(--surface-1);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
}

details.description > summary {
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  user-select: none;
  margin-bottom: 0;
}

details.description > summary::-webkit-details-marker { display: none; }

details.description > summary:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 4px;
  border-radius: 3px;
}

details.description > summary::before {
  content: "";
  width: 0;
  height: 0;
  border-left: 4px solid var(--text-3);
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform 0.15s ease;
}

details.description[open] > summary {
  margin-bottom: 10px;
}

details.description[open] > summary::before {
  transform: rotate(90deg);
}

.description-body {
  font-size: 13px;
  color: var(--text-2);
  line-height: 1.6;
  white-space: pre-wrap;
  word-break: break-word;
}

/* ── Thresholds ───────────────────────────────────────────────────────── */

.thresholds-form {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 18px 20px;
  background: var(--surface-1);
  border-radius: var(--radius-sm);
}

.threshold-row {
  display: grid;
  grid-template-columns: 14px 110px 1fr 70px;
  align-items: center;
  gap: 12px;
  font-size: 13px;
  color: var(--text);
}

.swatch {
  width: 10px;
  height: 10px;
  border-radius: 3px;
}

.swatch-red    { background: var(--red); }
.swatch-yellow { background: var(--yellow); }
.swatch-green  { background: var(--green); }

.threshold-value,
.threshold-range {
  color: var(--text);
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  text-align: right;
}

input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  height: 4px;
  background: rgba(255,255,255,0.1);
  border-radius: 2px;
  outline: none;
}

/* Keyboard focus indicator (WCAG 2.4.7). The bare `outline: none` above
   left keyboard users with no visible focus on the pay-threshold sliders;
   the thumb gets a blue focus ring in both engines. */
input[type="range"]:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 3px var(--bg), 0 0 0 5px var(--blue);
}
input[type="range"]:focus-visible::-moz-range-thumb {
  box-shadow: 0 0 0 3px var(--bg), 0 0 0 5px var(--blue);
}

input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--accent);
  cursor: pointer;
  border: 2px solid var(--panel);
}

input[type="range"]::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--accent);
  cursor: pointer;
  border: 2px solid var(--panel);
}

.thresholds-form .btn {
  align-self: flex-start;
  margin-top: 4px;
}

/* ── AI booking assessment (consolidated scoring + rationale) ──────────────
   This block replaces the old top-of-panel .scores-row + bottom-of-panel
   .reasoning split. Both Bookability and Worth It render as side-by-side
   .score-card components: each carries the dimension label, a hint line,
   the color-coded numeric value (/100), and the AI rationale below. The
   "Refresh scores" button lives in this section's header so it sits with
   the thing it regenerates. */

.ai-assessment {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.ai-assessment-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}

.ai-assessment-head .section-heading {
  margin-bottom: 4px;
  font-size: 12px;
  color: var(--text);
}

.ai-assessment-sub {
  margin: 0;
  font-size: 11.5px;
  color: var(--text-3);
  line-height: 1.4;
}

/* Groups the "Re-scan details" + "Refresh" buttons on the right of the
   assessment header so the flex `justify-content: space-between` keeps the
   title on the left and both actions together on the right. */
.ai-assessment-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

/* Reply-enrichment note (detail panel): a Discord reply overwrote a field.
   Amber tint echoes the relay's amber "getting contacts" alert — same
   "something changed on this listing" signal. */
.reply-enrich-note {
  padding: 12px 14px;
  background: var(--amber-bg);
  border: 1px solid var(--border);
  border-left: 3px solid var(--amber);
  border-radius: var(--radius-sm);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.reply-enrich-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
}
.reply-enrich-icon { font-size: 13px; }
.reply-enrich-title {
  font-weight: 600;
  font-size: 12.5px;
  color: var(--text);
}
.reply-enrich-time {
  font-size: 11px;
  color: var(--text-3);
}
.reply-enrich-changes {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.reply-enrich-changes li {
  font-size: 12px;
  color: var(--text-2);
  display: flex;
  align-items: baseline;
  gap: 6px;
  flex-wrap: wrap;
}
.reply-enrich-field { font-weight: 600; color: var(--text); }
.reply-enrich-old { color: var(--text-3); text-decoration: line-through; }
.reply-enrich-arrow { color: var(--text-3); }
.reply-enrich-new { color: var(--text); font-weight: 600; }
.reply-enrich-actions {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.reply-enrich-link { font-size: 11.5px; color: var(--blue); }

.ai-assessment-empty {
  padding: 20px 16px;
  background: var(--surface-1);
  border-radius: var(--radius-sm);
  font-size: 13px;
  color: var(--text-3);
  text-align: center;
}

.score-cards {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

@media (max-width: 920px) {
  .score-cards { grid-template-columns: 1fr; }
}

/* Likelihood-of-landing card spans the full grid width, sitting below the
   two fit axes (bookability + worth-it). It's a relationship-derived
   secondary metric — the full-width span signals "summary line" while the
   band colour + quote rationale stay identical to the two cards above. In
   the single-column mobile layout grid-column:1/-1 is a no-op (already
   full width), so the cards just stack. */
.score-card-success {
  grid-column: 1 / -1;
}

/* Each card uses a 3px colored left accent + a subtle background wash to
   group its contents — no outer border. The accent recolors with the
   score band so green/amber/red is legible at a glance, matching the
   listing-card .stale pattern. Background lift (`--surface-1`) carries
   the grouping that the outer border used to. */
.score-card {
  background: var(--surface-1);
  border-left: 3px solid var(--text-3);
  border-radius: var(--radius-sm);
  padding: 16px 18px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
  transition: background-color 0.2s ease, border-color 0.2s ease;
}

.score-card-green {
  border-left-color: var(--green);
  background: linear-gradient(to right, rgba(103,214,140,0.08), rgba(255,255,255,0.03) 55%);
}

.score-card-amber {
  border-left-color: var(--amber);
  background: linear-gradient(to right, rgba(240,169,71,0.08), rgba(255,255,255,0.03) 55%);
}

.score-card-red {
  border-left-color: var(--red);
  background: linear-gradient(to right, rgba(255,107,107,0.08), rgba(255,255,255,0.03) 55%);
}

.score-card-none {
  border-left-color: var(--text-3);
}

.score-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border);
}

.score-card-meta {
  display: flex;
  flex-direction: column;
  gap: 3px;
  flex: 1;
  min-width: 0;
}

.score-card-label {
  font-size: 11px;
  font-weight: 600;
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
}

.score-card-hint {
  font-size: 12px;
  color: var(--text);
  font-weight: 500;
  line-height: 1.35;
}

/* The big number is the headline of the card — sized for at-a-glance
   reading from across the room. /100 unit stays small so the digits
   carry the visual weight. */
.score-card-value {
  display: inline-flex;
  align-items: baseline;
  font-size: 30px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  padding: 2px 12px;
  border-radius: var(--radius-sm);
  flex: 0 0 auto;
  line-height: 1;
  letter-spacing: -0.02em;
  transition: background-color 0.2s ease, color 0.2s ease;
}

.score-card-out {
  font-size: 11px;
  font-weight: 500;
  opacity: 0.6;
  margin-left: 3px;
  letter-spacing: 0;
}

/* Rationale paragraph. Kept inside a card so its line length is roughly
   half the panel width — easier to scan than the old full-width prose.
   The hanging left rule + indent frames the sentence as the model's
   reasoning (a quote/callout) so it reads as the "why" the section
   header promises rather than generic body copy. Neutral accent — not
   band-tinted — so the card's background gradient + 3px left border stay
   the only colour-coded signals and the band reading isn't muddied. */
.score-card-body {
  margin: 0;
  padding-left: 12px;
  border-left: 2px solid var(--border-strong);
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--text);
  white-space: pre-wrap;
  word-break: break-word;
}

/* Empty state: fainter rule + muted italic so a not-yet-scored card
   doesn't pull the eye the way a real rationale does. */
.score-card-body.muted {
  border-left-color: var(--border);
  color: var(--text-3);
  font-style: italic;
}

.btn-sm {
  padding: 4px 10px;
  font-size: 12px;
}

.muted { color: var(--text-3); }

/* ── Outreach ─────────────────────────────────────────────────────────── */

.outreach-details {
  display: block;
}

.outreach-summary {
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  user-select: none;
}

.outreach-summary::-webkit-details-marker { display: none; }

.outreach-summary:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 4px;
  border-radius: 3px;
}

.outreach-summary::before {
  content: "";
  width: 0;
  height: 0;
  border-left: 4px solid var(--text-3);
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform 0.15s ease;
}

.outreach-details[open] > .outreach-summary::before {
  transform: rotate(90deg);
}

.outreach-summary .section-heading {
  margin-bottom: 0;
}

.outreach-details[open] > .outreach-summary {
  margin-bottom: 10px;
}

.outreach-form textarea {
  width: 100%;
  background: rgba(0, 0, 0, 0.25);
  color: var(--text);
  border: 1px solid var(--input-border);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  font: inherit;
  font-size: 14px;
  line-height: 1.6;
  letter-spacing: 0;
  resize: vertical;
  min-height: 200px;
  transition: border-color 0.14s ease, box-shadow 0.14s ease;
}

.outreach-form textarea:hover {
  background: rgba(0, 0, 0, 0.30);
}

.outreach-form textarea:focus {
  outline: none;
  border-color: rgba(120, 170, 255, 0.5);
  box-shadow: 0 0 0 2px rgba(120, 170, 255, 0.18);
}

.outreach-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
  flex-wrap: wrap;
}

/* Send Email is the high-commitment action — push it to the right of the
   outreach action row so it's visually separated from Copy / Save Draft.
   The "Last sent…" meta text trails after it. */
.outreach-actions .btn-send {
  margin-left: auto;
}

.outreach-meta {
  margin: 6px 0 0;
  font-size: 12px;
}

.muted-warn {
  color: var(--amber);
}

.btn[disabled],
.btn[disabled]:hover {
  opacity: 0.45;
  cursor: not-allowed;
  background: transparent;
  color: var(--text-3);
  border-color: var(--border);
  transform: none;
  box-shadow: none;
}

.btn-primary[disabled],
.btn-primary[disabled]:hover,
.btn-send[disabled],
.btn-send[disabled]:hover,
.btn-danger[disabled],
.btn-danger[disabled]:hover {
  background: rgba(255,255,255,0.06);
  color: var(--text-3);
  border-color: var(--border);
  box-shadow: none;
}

/* ── Recipients grid (TO/CC/BCC inside the outreach form) ─────────────── */
/*
   Stacked layout: label sits directly above its input so the eye doesn't
   travel across a wide left column. The To row may also contain an inline
   .outreach-alert as a third child when no recipient is saved — this keeps
   the warning visually anchored to the field it concerns. */

.recipients-grid {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-bottom: 18px;
}

.recipient-row {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.recipient-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-2);
  font-weight: 500;
}

.recipient-row input[type="text"] {
  width: 100%;
  background: rgba(0, 0, 0, 0.25);
  color: var(--text);
  border: 1px solid var(--input-border);
  border-radius: var(--radius-sm);
  padding: 9px 12px;
  font: inherit;
  font-size: 14px;
  transition: border-color 0.14s ease, background-color 0.14s ease,
              box-shadow 0.14s ease;
}

.recipient-row input[type="text"]:hover {
  background: rgba(0, 0, 0, 0.30);
}

.recipient-row input[type="text"]:focus {
  outline: none;
  border-color: rgba(120, 170, 255, 0.5);
  box-shadow: 0 0 0 2px rgba(120, 170, 255, 0.18);
}

.recipient-row input::placeholder {
  color: var(--text-3);
}

/* Compact alert anchored under the To input. Amber accent so it reads as
   "fix this before continuing" without screaming. The .env fallback line
   is intentionally smaller / muted — informational, not the headline. */
.outreach-alert {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin-top: 4px;
  padding: 8px 12px;
  background: var(--amber-bg);
  border-left: 3px solid var(--amber);
  border-radius: var(--radius-sm);
}

.outreach-alert-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  margin-top: 1px;
  border-radius: 50%;
  background: var(--amber);
  color: #1e1e1e;
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
}

.outreach-alert-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.outreach-alert-primary {
  font-size: 13px;
  color: var(--text);
  font-weight: 500;
}

.outreach-alert-detail {
  font-size: 11px;
  color: var(--text-3);
}

/* ── Draft editor (textarea + its inline header) ──────────────────────── */

.draft-editor {
  margin-top: 4px;
}

.draft-editor-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 8px;
}

.draft-editor-head .section-heading {
  margin-bottom: 0;
}

.draft-editor-meta {
  font-size: 11px;
  color: var(--text-3);
  white-space: nowrap;
}

/* ── Message history ──────────────────────────────────────────────────── */

.message-history {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.msg {
  background: var(--surface-1);
  border-left: 3px solid var(--green);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
}

.msg-failed {
  border-left-color: var(--red);
}

.msg-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
}

.msg-time {
  font-size: 12px;
  color: var(--text-3);
}

.msg-meta {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin: 0 0 8px;
  font-size: 12px;
}

.msg-meta div {
  display: grid;
  grid-template-columns: 60px 1fr;
  gap: 8px;
}

.msg-meta dt {
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  font-size: 10.5px;
  align-self: center;
}

.msg-meta dd {
  margin: 0;
  color: var(--text-2);
  word-break: break-word;
}

.msg-error {
  color: var(--red);
}

.msg-body-toggle {
  margin-top: 4px;
}

.msg-body-toggle summary {
  font-size: 12px;
  color: var(--text-3);
  cursor: pointer;
  padding: 2px 0;
}

.msg-body-toggle summary:hover {
  color: var(--text-2);
}

.msg-body-toggle summary:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
  border-radius: 3px;
  color: var(--text-2);
}

.msg-body {
  margin: 8px 0 0;
  padding: 10px 12px;
  background: rgba(0,0,0,0.30);
  border-radius: var(--radius-sm);
  color: var(--text-2);
  font-size: 12px;
  line-height: 1.55;
  white-space: pre-wrap;
  word-break: break-word;
  font-family: inherit;
  max-height: 400px;
  overflow-y: auto;
}

/* ── Status buttons ───────────────────────────────────────────────────── */

.status-buttons {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.inline { display: inline-block; }

/* ── Action row ───────────────────────────────────────────────────────── */
/*
   Two visible slots + one collapsed menu:
     .action-primary   — Draft outreach. Solid blue CTA, always first.
     (Mark replied)    — surfaces only when status='contacted' so the
                         user can clear the stale indicator inline.
     .action-snoozed-tag — small badge that shows the wake date when the
                         listing is currently snoozed (lives next to the
                         primary CTA so the state is visible without
                         opening the menu).
     .action-more      — collapsed dropdown holding every remaining
                         status flip (Shortlist / Mark contacted / Mark
                         booked), snooze controls, and Dismiss. Pushed
                         to the right edge via margin-left:auto so the
                         primary CTA owns the left side.

   Native <details>+<summary> drives the menu; the JS hook in app.js
   adds Esc-to-close + outside-click-to-close. Each menu item is a
   form-button posting to the same routes the inline buttons used, so
   CSRF + redirect behavior is unchanged. */

.action-row {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  align-items: center;
}

.action-primary {
  flex: 0 0 auto;
}

.btn-action {
  font-size: 12px;
  padding: 6px 12px;
}

/* Used by the bulk-action-bar buttons on the board ("Shortlist selected"
   / "Dismiss selected"). These class hooks survived the detail-panel
   action-row redesign because the bulk bar still needs an amber-hover
   shortlist affordance and a persistent-red dismiss treatment. */
.btn-action-shortlisted:hover {
  border-color: var(--amber);
  color: var(--amber);
}

.btn-action-dismissed {
  border-color: rgba(255, 107, 107, 0.32);
  color: var(--red);
}

.btn-action-dismissed:hover {
  background: var(--red-bg);
  border-color: var(--red);
  color: var(--red);
}

.btn-action-dismissed:focus-visible {
  outline-color: var(--red);
}

/* Draft outreach is the primary call-to-action in the detail panel — solid
   blue fill so it reads clearly as "the next thing to do here" against the
   outline buttons on the same row. Sized slightly larger (font + padding)
   for additional visual weight. */
.btn-action-draft {
  /* Darker-than-brand blue (--blue #5fa8ff fails white-text contrast at
     2.46:1). #2b74cf gives white text 4.66:1 — clears WCAG AA 1.4.3 while
     still reading as the primary blue CTA. The bright --blue stays the
     accent for focus rings / colored text elsewhere where it sits on dark
     backgrounds, not behind white text. */
  background: #2b74cf;
  border-color: #2b74cf;
  color: #fff;
  font-weight: 600;
  font-size: 13px;
  padding: 8px 18px;
  /* Override the inherited .btn transition specifically for the box-shadow
     so the glow eases in smoothly (220ms ease-out-expo) rather than the
     default 160ms ease. Other properties keep the inherited timings. */
  transition: background 0.14s ease, border-color 0.14s ease,
              color 0.14s ease,
              box-shadow 0.22s cubic-bezier(0.16, 1, 0.3, 1),
              transform 0.1s ease;
}

/* Glow + saturated tint on hover AND keyboard focus. Two-layer shadow:
   a soft outer halo carries the "premium" feel; a subtle inset white
   highlight adds depth without enlarging the button. The glow only
   appears in these interactive states — at rest the button is flat,
   so the page never feels gimmicky or animated. */
.btn-action-draft:hover,
.btn-action-draft:focus-visible {
  background: #2769bd;
  border-color: #2769bd;
  color: #fff;
  box-shadow: 0 4px 18px rgba(95, 168, 255, 0.38),
              inset 0 0 0 1px rgba(255, 255, 255, 0.10);
}

/* Keyboard focus gets the lift in addition to the glow so it matches
   the hover state — same affordance, two input modalities. The 2px
   blue outline ring comes from .btn:focus-visible above. */
.btn-action-draft:focus-visible {
  transform: translateY(-1px);
}

.btn-action-mark-replied:hover {
  border-color: var(--green);
  color: var(--green);
}

/* "✓ Replied" stays in the green-tinted state until the user clicks to
   clear, so the affirmative state is obvious without hover. */
.btn-action-replied {
  border-color: rgba(103, 214, 140, 0.35);
  color: var(--green);
  background: var(--green-bg);
}
.btn-action-replied:hover {
  border-color: var(--green);
  background: rgba(103, 214, 140, 0.20);
}

/* Snoozed indicator next to the primary CTA. Blue pill matching the
   accent so the state reads at a glance without opening More. */
.action-snoozed-tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11.5px;
  font-weight: 500;
  color: var(--blue);
  background: var(--blue-bg);
  padding: 3px 9px;
  border-radius: 999px;
  letter-spacing: 0.01em;
}

.action-snoozed-icon {
  font-size: 11px;
  line-height: 1;
}

/* ── More menu (compact dropdown) ─────────────────────────────────────── */

.action-more {
  position: relative;
  margin-left: auto;
}

.action-more-toggle {
  list-style: none;
  cursor: pointer;
  user-select: none;
}

.action-more-toggle::-webkit-details-marker { display: none; }
.action-more-toggle::marker { content: ""; }

.action-more-dots {
  font-size: 16px;
  line-height: 1;
  letter-spacing: 0.08em;
  margin-right: -2px;
}

.action-more[open] > .action-more-toggle {
  border-color: var(--border-strong);
  background: var(--surface-2);
}

.action-more-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 20;
  min-width: 240px;
  padding: 5px;
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45);
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.action-more-form {
  display: block;
  margin: 0;
}

.action-more-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 8px 12px;
  background: transparent;
  border: 0;
  border-radius: 5px;
  color: var(--text);
  font: inherit;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: background-color 0.12s ease, color 0.12s ease;
}

.action-more-item:hover {
  background: rgba(255, 255, 255, 0.06);
}

.action-more-item:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: -2px;
  background: rgba(255, 255, 255, 0.05);
}

.action-more-item-danger {
  color: var(--red);
}

.action-more-item-danger:hover {
  background: var(--red-bg);
  color: var(--red);
}

.action-more-item-danger:focus-visible {
  outline-color: var(--red);
}

.action-more-divider {
  height: 1px;
  background: var(--border);
  margin: 4px -5px;
}

.action-more-section {
  padding: 6px 8px 4px;
}

.action-more-section-label {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-3);
  margin-bottom: 6px;
}

.action-more-snooze-quick {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  margin: 0 0 6px;
}

.action-more-snooze-quick .btn,
.action-more-snooze-custom .btn {
  flex: 1 1 auto;
  font-size: 11.5px;
  padding: 5px 8px;
}

.action-more-snooze-custom {
  display: flex;
  gap: 4px;
  align-items: center;
}

.action-more-snooze-custom input[type="date"] {
  flex: 2 1 0;
  min-width: 0;
  background: rgba(0, 0, 0, 0.25);
  color: var(--text);
  border: 1px solid var(--input-border);
  border-radius: 6px;
  padding: 5px 6px;
  font: inherit;
  font-size: 12px;
  transition: border-color 0.14s ease, box-shadow 0.14s ease;
}

.action-more-snooze-custom input[type="date"]:focus {
  outline: none;
  border-color: rgba(120, 170, 255, 0.5);
  box-shadow: 0 0 0 2px rgba(120, 170, 255, 0.18);
}

.wake-pill {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  background: var(--blue-bg);
  color: var(--blue);
}

/* ── Notes ────────────────────────────────────────────────────────────── */
/*
   Notes default to a single-line "Add a private note…" affordance so an
   empty notes section doesn't dominate the panel. The textarea expands
   when (a) it has focus or (b) it already carries saved content
   (:not(:placeholder-shown) — true whenever the textarea's value is
   non-empty). The Save Note button hides on the same condition so the
   row stays quiet at rest.

   Accessibility: :focus-within triggers on keyboard focus, so Tab into
   the textarea expands it and reveals Save. The compact mode keeps the
   resize handle off; expanded mode re-enables vertical resize for long
   notes. The aria-label on the textarea covers the case where the
   section heading isn't read in screen-reader form-control context. */

.note-form {
  display: flex;
  flex-direction: column;
}

.note-form textarea {
  width: 100%;
  min-height: 38px;
  background: rgba(0, 0, 0, 0.22);
  color: var(--text);
  border: 1px solid var(--input-border);
  border-radius: var(--radius-sm);
  padding: 8px 12px;
  font: inherit;
  font-size: 13px;
  line-height: 1.5;
  resize: none;
  overflow: hidden;
  transition: min-height 0.18s ease, border-color 0.14s ease,
              background-color 0.14s ease;
}

.note-form textarea:hover {
  border-color: var(--border-strong);
}

.note-form:focus-within textarea,
.note-form textarea:not(:placeholder-shown) {
  min-height: 96px;
  resize: vertical;
  overflow: auto;
  background: rgba(0, 0, 0, 0.28);
}

.note-form textarea:focus {
  outline: none;
  border-color: rgba(120, 170, 255, 0.5);
  box-shadow: 0 0 0 2px rgba(120, 170, 255, 0.18);
}

.note-actions {
  display: flex;
  justify-content: flex-end;
  max-height: 0;
  margin-top: 0;
  overflow: hidden;
  opacity: 0;
  transition: max-height 0.18s ease, opacity 0.18s ease,
              margin-top 0.18s ease;
}

.note-form:focus-within .note-actions,
.note-form textarea:not(:placeholder-shown) ~ .note-actions {
  max-height: 60px;
  margin-top: 8px;
  opacity: 1;
}

/* ── New listing form ─────────────────────────────────────────────────── */

/* Entry panel mirrors the settings panel but is wider (manual-listing form
   has more fields and benefits from two-column rows on desktop). Capped so
   inputs don't span ergonomically painful widths on a 1440+ display. */
.entry-board {
  justify-content: flex-start;
}

.entry-panel {
  flex: 1;
  min-width: 0;
  padding: 28px 32px;
  /* No max-width on purpose — these pages (Profile, Admin, Stats, manual
     entry, Ratings) are forms / tables / card grids that benefit from
     the full main-area width on wide monitors. Listing-detail prose has
     its own readability cap on `.detail` and is unaffected. If a
     specific row reads too wide, cap it on the row, not the panel. */
}

.entry-content {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.entry-required-note {
  margin: -6px 0 0;
  font-size: 12px;
  color: var(--text-2);
}

.entry-required-note .required {
  color: var(--red);
  margin-right: 4px;
}

.entry-form {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 22px 24px 0;
  background: rgba(255,255,255,0.02);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  position: relative;
}

.form-section {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 18px 0 22px;
  border-bottom: 1px solid var(--border);
}

.form-section:first-of-type {
  padding-top: 4px;
}

.form-section:last-of-type {
  border-bottom: none;
  padding-bottom: 6px;
}

.form-section .section-heading {
  margin: 0 0 2px;
}

.entry-row {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}

.entry-row label,
.entry-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-3);
}

.entry-label.is-required {
  color: var(--text);
}

.entry-row .required {
  color: var(--red);
  margin-left: 2px;
}

.entry-help {
  margin: 2px 0 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--text-3);
}

.entry-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
}

.entry-grid--city-state {
  grid-template-columns: 2fr 1fr;
}

@media (max-width: 720px) {
  .entry-grid,
  .entry-grid--city-state {
    grid-template-columns: 1fr;
  }
}

/* Form-input dark-theme parity: text-like inputs, number inputs,
   selects, and textareas all share the same base treatment. We strip
   native widget chrome via `appearance: none` (Safari and Chrome
   otherwise paint their own light/native backgrounds over ours,
   especially on <select> and <input type="number">) and supply a
   custom dropdown arrow on selects below.
   `.board-defaults-form` (on /settings) is folded in here because its
   selects would otherwise render as native white widgets — same fix
   pattern as /profile, just for a form that doesn't carry .entry-form. */
.entry-form input[type="text"],
.entry-form input[type="url"],
.entry-form input[type="email"],
.entry-form input[type="tel"],
.entry-form input[type="number"],
.entry-form input[type="search"],
.entry-form input[type="date"],
.entry-form input[type="password"],
.entry-form select,
.entry-form textarea,
.board-defaults-form select {
  width: 100%;
  background-color: rgba(0, 0, 0, 0.25);
  color: var(--text);
  border: 1px solid var(--input-border);
  border-radius: var(--radius-sm);
  padding: 9px 12px;
  font: inherit;
  font-size: 13px;
  line-height: 1.4;
  transition: border-color 0.14s ease, background-color 0.14s ease,
              box-shadow 0.14s ease;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
}

/* Custom dropdown arrow because `appearance: none` strips the native
   chevron. Inline SVG keeps this asset-free; the right-padding leaves
   room so option text doesn't clash with the arrow. */
.entry-form select,
.board-defaults-form select {
  padding-right: 32px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8' fill='none' stroke='%23c0c0c0' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='1,1.5 6,6.5 11,1.5'/></svg>");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 10px 7px;
}

.entry-form textarea {
  resize: vertical;
  min-height: 180px;
  line-height: 1.5;
}

.entry-form input:hover,
.entry-form select:hover,
.entry-form textarea:hover,
.board-defaults-form select:hover {
  background-color: rgba(0, 0, 0, 0.30);
}

.entry-form input:focus,
.entry-form select:focus,
.entry-form textarea:focus,
.board-defaults-form select:focus {
  outline: none;
  border-color: rgba(120, 170, 255, 0.5);
  box-shadow: 0 0 0 2px rgba(120, 170, 255, 0.18);
}

.entry-form input::placeholder,
.entry-form textarea::placeholder {
  color: var(--text-3);
}

/* Color the native calendar / spin / search-cancel pseudo-controls to
   match the dark theme. Without this they render in the OS's light
   palette and look pasted on. */
.entry-form input[type="date"]::-webkit-calendar-picker-indicator,
.entry-form input[type="search"]::-webkit-search-cancel-button {
  filter: invert(0.7);
  cursor: pointer;
}

.entry-actions {
  position: sticky;
  bottom: 0;
  display: flex;
  gap: 10px;
  padding: 14px 24px 16px;
  margin: 4px -24px 0;
  background: var(--panel);
  border-top: 1px solid var(--border);
  box-shadow: 0 -8px 12px -10px rgba(0,0,0,0.5);
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
}

/* Visibility radio group on /listings/new (Phase 7). Two side-by-side
   options. The whole label is clickable; the radio sits above an inline
   title + help-text body. Keeps the visual rhythm of the rest of the
   form while making the choice obvious at a glance. */
.entry-radio-group {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

.entry-radio {
  flex: 1 1 240px;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px 14px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.02);
  cursor: pointer;
  transition: border-color 0.14s ease, background 0.14s ease;
}

.entry-radio:hover { border-color: var(--text-3); }

.entry-radio:has(input:checked) {
  border-color: var(--blue);
  background: var(--blue-bg);
}

.entry-radio.is-disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.entry-radio.is-disabled:hover { border-color: var(--border-strong); }

.entry-radio input[type="radio"] {
  margin-top: 2px;
  accent-color: var(--blue);
}

.entry-radio-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.entry-radio-title {
  font-weight: 600;
  color: var(--text);
}

.entry-radio-help {
  font-size: 12px;
  color: var(--text-3);
}

/* ── Original link ────────────────────────────────────────────────────── */

.original-link {
  color: var(--text-2);
  text-decoration: none;
  font-size: 13px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}

.original-link:hover { color: var(--text); }

.original-link:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
  color: var(--text);
  border-radius: 4px;
}

/* ── Status banner (scraper run feedback + other flash messages) ─────────
   Replaces the old monospace .flash block. The helper render_flash in
   app.rb parses "[ts] Scraper finished. N saved, M updated, K errors."
   into a structured "Last run: <time> · N saved · M updated · K errors"
   banner; unrecognized messages fall through to the same banner with a
   neutral kind. The leading dot is a status indicator that recolors per
   kind (green/amber/red/blue). */

.status-banner {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  margin: 12px 24px 0;
  padding: 10px 14px;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: 13px;
  letter-spacing: 0;
}

/* The HTML `hidden` attribute only hides via the UA `[hidden]{display:none}`
   rule, which ties on specificity with `.status-banner` above and LOSES the
   cascade (author styles come after UA styles). Without this override a
   `.status-banner[hidden]` (e.g. the scoring-progress banner the server
   renders hidden when no sweep is running) would show anyway — and the JS
   poller, seeing `banner.hidden === true`, would never re-hide it, so the
   banner stuck on every page load forever. Same trap as
   `.bulk-action-bar[hidden]` further up. */
.status-banner[hidden] { display: none; }

.status-banner::before {
  content: "";
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--text-3);
}

.status-banner-success::before {
  background: var(--green);
  box-shadow: 0 0 0 3px rgba(103, 214, 140, 0.15);
}
.status-banner-warning::before {
  background: var(--amber);
  box-shadow: 0 0 0 3px rgba(240, 169, 71, 0.18);
}
.status-banner-error::before {
  background: var(--red);
  box-shadow: 0 0 0 3px rgba(255, 107, 107, 0.18);
}
.status-banner-info::before {
  background: var(--blue);
  box-shadow: 0 0 0 3px rgba(95, 168, 255, 0.18);
}

.status-banner-success {
  background: linear-gradient(to right, var(--green-bg), rgba(255,255,255,0.03) 45%);
  border-color: rgba(103, 214, 140, 0.22);
}

.status-banner-warning {
  background: linear-gradient(to right, var(--amber-bg), rgba(255,255,255,0.03) 45%);
  border-color: rgba(240, 169, 71, 0.25);
}

.status-banner-error {
  background: linear-gradient(to right, var(--red-bg), rgba(255,255,255,0.03) 45%);
  border-color: rgba(255, 107, 107, 0.28);
}

.status-banner-info {
  background: linear-gradient(to right, var(--blue-bg), rgba(255,255,255,0.03) 45%);
  border-color: rgba(95, 168, 255, 0.22);
}

/* The first part is the timestamped label ("Last run: …"); render it
   with slightly stronger weight so the bullet-separated stats that
   follow read as supporting detail. */
.status-banner-part:first-of-type {
  font-weight: 600;
  letter-spacing: 0.01em;
}

/* "AI scoring in background" banner — same chrome as .status-banner-info
   with a small inline spinner. The spinner animation reuses the global
   backstage-spin keyframe and is collapsed by the prefers-reduced-motion
   block at the bottom of this stylesheet. */
.scoring-progress-spinner {
  display: inline-block;
  width: 0.85em;
  height: 0.85em;
  margin-right: 0.5em;
  vertical-align: -0.1em;
  border: 2px solid rgba(95, 168, 255, 0.35);
  border-top-color: rgba(95, 168, 255, 0.95);
  border-radius: 50%;
  animation: backstage-spin 0.9s linear infinite;
}

.status-banner-part {
  color: var(--text);
}

.status-banner-sep {
  color: var(--text-3);
  font-weight: 400;
}

.status-banner-link {
  color: var(--blue);
  text-decoration: none;
  font-weight: 500;
}

.status-banner-link:hover {
  text-decoration: underline;
}

/* ── Responsive (tablet + mobile) ────────────────────────────────────────
   Two breakpoints: 1024px tightens the desktop layout for narrow laptops;
   880px is the "real" mobile cut where the two-panel board collapses to a
   stacked column and the sidebar goes off-canvas behind a hamburger.

   Load-bearing details:
   - At 880px we flip body to display:block (so .main fills the page) and
     drop the html/body height:100% lock so page-scroll replaces the
     desktop's panel-internal scroll. Without this, content past the
     viewport gets clipped instead of scrolled.
   - Topbar wraps; filter tabs become an x-scrolling row so all six fit on
     phone widths without truncating. Sort + filters dropdowns and the
     action buttons share the wrapped row below.
   - Listings panel is capped at 60vh on mobile so the detail panel below
     it is reachable without scrolling past the entire list. */

@media (max-width: 1024px) {
  .listings-panel {
    width: 320px;
    min-width: 300px;
    flex: 0 0 320px;
  }

  .detail {
    padding: 20px 22px 24px;
  }


  .topbar {
    gap: 8px;
    padding: 10px 14px;
  }
}

@media (max-width: 880px) {
  html, body {
    height: auto;
    min-height: 100%;
  }

  body {
    display: block;
  }

  .menu-toggle {
    display: inline-flex;
  }

  /* The floating .menu-toggle is fixed at top:12px height:40px (bottom edge
     at 52px). Pad the sidebar's top so the wordmark clears it with breathing
     room — 70px gives ~18px of vertical air, matching the topbar's rhythm. */
  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    height: 100vh;
    width: 240px;
    min-width: 240px;
    z-index: 1000;
    transform: translateX(-100%);
    transition: transform 0.2s ease;
    box-shadow: 4px 0 24px rgba(0, 0, 0, 0.45);
    padding-top: 70px;
  }

  body.sidebar-open .sidebar {
    transform: translateX(0);
  }

  .sidebar-backdrop {
    display: block;
  }

  body.sidebar-open .sidebar-backdrop {
    opacity: 1;
    pointer-events: auto;
  }

  .main {
    width: 100%;
    height: auto;
    min-height: 100vh;
    overflow: visible;
  }

  /* Topbar wraps onto multiple rows. Padding-left leaves room for the
     hamburger so the title doesn't sit underneath it. */
  .topbar {
    flex-wrap: wrap;
    padding: 12px 14px 10px 60px;
    gap: 8px 10px;
  }

  .topbar h1 {
    font-size: 17px;
  }

  .topbar-title {
    flex: 1 1 auto;
    min-width: 0;
  }

  .topbar-sub {
    display: none;
  }

  /* Filter tabs take a full row and scroll horizontally on overflow so
     all six options stay reachable on a phone. The sort + filters
     dropdowns share the next row with the action buttons. */
  .topbar-tabs {
    flex: 1 1 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
    padding-bottom: 2px;
  }

  .topbar-tabs::-webkit-scrollbar {
    height: 4px;
  }

  .topbar-tabs .tab {
    flex: 0 0 auto;
    white-space: nowrap;
  }

  .topbar-sort-panel,
  .topbar-views-panel {
    flex: 0 0 auto;
  }

  .topbar-actions {
    margin-left: auto;
  }

  .status-banner {
    margin: 12px 16px 0;
  }

  /* Two-panel board stacks: listings on top, detail/settings/entry below.
     `.panel` keeps its scroll for the listings card list (capped via
     max-height) but the detail panel grows naturally with page scroll. */
  .board {
    flex-direction: column;
    overflow: visible;
    padding: 12px;
    gap: 12px;
  }

  .listings-panel,
  .detail-panel,
  .settings-panel,
  .entry-panel {
    width: 100%;
    min-width: 0;
    max-width: none;
    flex: 0 0 auto;
  }

  /* Phone master-detail. The board (/) shows the list full-screen and hides
     the detail; an open listing (/listings/:id) shows the detail full-screen
     and hides the list. Route-driven via .board-detail-open (set in
     index.erb) — no JS. The list flows with the page rather than its own
     60vh well, so there's no nested scroll to fight. */
  .board:not(.board-detail-open) .detail-panel {
    display: none;
  }

  .board.board-detail-open .listings-panel,
  .board.board-detail-open .board-resizer {
    display: none;
  }

  .board:not(.board-detail-open) .listings-panel {
    max-height: none;
    overflow: visible;
  }

  .board:not(.board-detail-open) .listings-scroll {
    overflow: visible;
  }

  .detail-panel,
  .settings-panel {
    overflow: visible;
  }

  /* Sticky "← Board" bar atop a full-screen detail. Indented past the fixed
     hamburger (40px @ left:12px, z-index 1001) so the label clears it when
     the bar pins to the viewport top mid-scroll. Full-width so its panel-
     coloured background covers content scrolling underneath. */
  .board-detail-open .detail-back {
    display: flex;
    align-items: center;
    gap: 6px;
    position: sticky;
    top: 0;
    z-index: 30;
    margin: -4px 0 14px;
    padding: 9px 14px 9px 46px;
    background: var(--panel);
    border-bottom: 1px solid var(--border);
    color: var(--text-2);
    font-size: 13px;
    font-weight: 600;
    text-decoration: none;
  }

  .board-detail-open .detail-back:hover {
    color: var(--text);
  }

  .board-detail-open .detail-back:focus-visible {
    outline: 2px solid var(--blue);
    outline-offset: -2px;
  }

  .detail-back-arrow {
    font-size: 15px;
    line-height: 1;
  }

  .settings-panel {
    padding: 18px 18px;
  }

  /* On mobile the panel itself doesn't scroll (page-scroll takes over),
     so sticky on .detail-header would stick to the viewport — undesirable
     because the topbar already serves as orientation. Reset to static
     and let the header flow normally with the rest of the panel content. */
  .detail {
    max-width: none;
    margin: 0;
    padding: 16px 16px 22px;
  }

  .detail-header {
    position: static;
    padding-bottom: 12px;
  }

  .entry-panel {
    overflow: visible;
    padding: 22px 20px;
  }

  /* Touch targets: bump padding/min-height on the controls fingers will
     actually hit. Listing cards already have a stretched-anchor overlay,
     so increasing card padding is enough — the whole card grows. */
  .btn {
    padding: 9px 14px;
    min-height: 38px;
  }

  .btn-sm {
    padding: 6px 12px;
    min-height: 32px;
  }

  .tab {
    padding: 8px 14px;
    font-size: 12.5px;
    min-height: 34px;
  }

  .listing-card {
    padding: 14px 14px 12px;
  }

  /* Per-card bulk-select checkbox is invisible-on-rest on desktop so it
     doesn't clutter the board. On touch there's no hover, so reveal it
     always — the tap target is what makes bulk select discoverable. */
  .listing-card-checkbox {
    opacity: 1;
  }

  .nav-item {
    min-height: 44px;
    font-size: 14px;
    padding: 12px 18px 12px 15px;
  }

  /* Detail panel internals: tighten the action row + outreach grid so
     they don't push the page sideways on small screens. */
  .action-row {
    gap: 8px;
  }

  /* On mobile the More menu is no longer width-constrained — let it span
     the available width when opened so the inline snooze controls have
     room to breathe. */
  .action-more-menu {
    min-width: 0;
    width: min(280px, calc(100vw - 32px));
  }

  .detail-meta {
    gap: 6px 14px;
  }

  /* Bulk action bar wraps so the primary buttons stay on their own line
     above the count + clear control. */
  .bulk-action-bar {
    flex-wrap: wrap;
  }

  .bulk-action-buttons {
    margin-left: 0;
    flex: 1 1 100%;
    order: 2;
  }

  /* The bar normally pins to the bottom of the list panel — but a full-screen
     phone list scrolls the whole page, so that's the far end of a long
     scroll. Pin it to the viewport bottom while a selection is active so
     "Shortlist / Dismiss selected" is always in reach. Lives inside the
     listings panel, which is hidden in detail mode, so it only ever shows on
     the board. */
  body.bulk-active .bulk-action-bar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 50;
    border-left: 0;
    border-top: 1px solid var(--border-strong);
    box-shadow: 0 -6px 20px rgba(0, 0, 0, 0.45);
  }

  /* Reserve space so the last rows aren't trapped behind the fixed bar. */
  body.bulk-active .board:not(.board-detail-open) .listings-scroll {
    padding-bottom: 88px;
  }

  /* Outreach send/edit is the key phone flow. Make Save Draft + Send big,
     even tap targets without touching the DOM order (Save Draft must stay
     the first submit so Enter saves rather than sends). Send keeps its
     distinct green outline; drop the margin-left:auto that would otherwise
     orphan it on a wrapped row. */
  .outreach-actions .btn-send {
    margin-left: 0;
  }

  .outreach-actions .btn-primary,
  .outreach-actions .btn-send {
    flex: 1 1 auto;
    min-height: 44px;
  }

  .outreach-actions .btn-ghost {
    flex: 0 0 auto;
  }

  /* 16px is the iOS Safari "no zoom on focus" floor. Apply it across the
     whole phone/tablet range (not just ≤480px) so an iPad portrait doesn't
     zoom when the outreach textarea or a recipient field gains focus. */
  .outreach-form textarea {
    min-height: 180px;
    font-size: 16px;
  }

  .recipient-row input[type="text"] {
    font-size: 16px;
  }
}

@media (max-width: 480px) {
  .topbar {
    padding: 12px 12px 10px 60px;
  }

  .topbar h1 {
    font-size: 16px;
  }

  .settings-panel,
  .entry-panel {
    padding: 16px 14px;
  }

  .detail {
    padding: 14px 14px 22px;
  }

  .board {
    padding: 10px;
    gap: 10px;
  }

  .detail-headline {
    font-size: 19px;
  }

  /* Single-column score cards already kick in via the existing
     @media (max-width: 920px) rule, so just collapse the recipients
     vertical rhythm a bit and keep the panel padding human. */
  .recipients-grid {
    gap: 10px;
  }

  /* font-size: 16px is the iOS Safari "no zoom on focus" floor — anything
     smaller triggers an unwanted page zoom when the textarea gains focus. */
  .outreach-form textarea {
    min-height: 180px;
    font-size: 16px;
  }

  .recipient-row input[type="text"],
  .entry-form input[type="text"],
  .entry-form input[type="url"],
  .entry-form input[type="email"],
  .entry-form input[type="tel"],
  .entry-form input[type="number"],
  .entry-form input[type="search"],
  .entry-form input[type="date"],
  .entry-form input[type="password"],
  .entry-form select,
  .entry-form textarea,
  .note-form textarea,
  .snooze-custom input[type="date"] {
    font-size: 16px;
  }
}

/* ── Animations & microinteractions ─────────────────────────────────────
   Entry animations are one-shot on mount only — nothing pulses or loops
   at rest. Hover/active/focus transitions live with their respective
   selectors above; this block only collects keyframes, the elements that
   use them, the Run Now loading state, and the prefers-reduced-motion
   override that switches all of it off.

   Keep durations short (120–200ms) so the UI never feels slow. */

@keyframes backstage-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes backstage-slide-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes backstage-spin {
  to { transform: rotate(360deg); }
}

/* ── Run Now skeleton scaffold ──────────────────────────────────────────
   When the user clicks Run Now, JS in app.js (startScraperLoadingState)
   swaps the listings-scroll contents for these skeleton cards and the
   detail panel for the .scan-running block below. Both stay visible
   until the server-side scrape finishes and the page reloads with real
   results. Body gets .scraper-running so any hover/select affordances
   on the (now-skeleton) listings panel can be neutralized via CSS. */

.skeleton-card {
  padding: 12px 14px 10px;
  border-radius: var(--radius-sm);
  border-left: 3px solid transparent;
  margin-bottom: 6px;
  background: rgba(255, 255, 255, 0.022);
  display: flex;
  gap: 10px;
  align-items: flex-start;
}

.skeleton-scores {
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex: 0 0 auto;
}

.skeleton-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
  padding-top: 2px;
}

/* Each .skeleton-line is the actual shimmer-animated block. Sizes below
   match the rough rhythm of a real listing card so the page doesn't jump
   when results land. */
.skeleton-line {
  background: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0.045) 0%,
    rgba(255, 255, 255, 0.11) 50%,
    rgba(255, 255, 255, 0.045) 100%
  );
  background-size: 200% 100%;
  border-radius: 4px;
  animation: skeleton-shimmer 1.5s ease-in-out infinite;
}

.skeleton-score    { width: 70px; height: 22px; border-radius: var(--radius-sm); }
.skeleton-headline { width: 78%;  height: 13px; }
.skeleton-subhead  { width: 56%;  height: 11px; opacity: 0.85; }
.skeleton-meta     { width: 90%;  height: 9px;  opacity: 0.65; }

@keyframes skeleton-shimmer {
  0%   { background-position: 100% 0; }
  100% { background-position: -100% 0; }
}

/* Hide the bulk-action affordances and listings header while the scrape
   is running — they don't apply to skeleton placeholders. */
body.scraper-running .listings-header,
body.scraper-running .bulk-action-bar {
  display: none;
}

/* Scan-running panel replaces .detail content during a scrape. Three
   pulsing blue dots + a title + a rotating message + a hint line. The
   .detail wrapper class is reused so the existing fade-in animation
   on .detail still plays and the panel padding matches. */
.scan-running {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 80px 24px;
  text-align: center;
  min-height: 280px;
}

.scan-running-icon {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

.scan-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--blue);
  opacity: 0.4;
  animation: scan-dot-pulse 1.2s ease-in-out infinite;
}

.scan-dot:nth-child(2) { animation-delay: 0.16s; }
.scan-dot:nth-child(3) { animation-delay: 0.32s; }

@keyframes scan-dot-pulse {
  0%, 100% { opacity: 0.3; transform: scale(1); }
  40%      { opacity: 1;   transform: scale(1.25); }
}

.scan-running-title {
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
  letter-spacing: -0.01em;
}

.scan-running-message {
  font-size: 13px;
  font-weight: 500;
  color: var(--text-2);
  /* Crossfade between rotating messages — JS sets opacity:0 → swaps text
     → sets opacity:1. min-height keeps the panel from height-jiggling as
     messages of different lengths swap in. */
  transition: opacity 0.18s ease;
  min-height: 1.5em;
}

.scan-running-hint {
  font-size: 12px;
  color: var(--text-3);
  margin-top: 4px;
  max-width: 28ch;
  line-height: 1.5;
}

/* Status-change pulse — runs once when JS adds .status-just-changed to a
   pill/chip after a successful status mutation. Brief scale + brightness
   bump that draws the eye without movement. Single play, ~220ms. */
@keyframes status-flash {
  0%   { transform: scale(1);    filter: brightness(1); }
  40%  { transform: scale(1.08); filter: brightness(1.4); }
  100% { transform: scale(1);    filter: brightness(1); }
}

.status-just-changed {
  animation: status-flash 220ms cubic-bezier(0.16, 1, 0.3, 1) both;
  /* transform-origin: left so the dot+text pill doesn't drift horizontally
     during the scale-up. Without this, the right edge of the pill moves
     and competes with surrounding content. */
  transform-origin: left center;
}

/* Detail panel content fades in when the user picks a different listing.
   Opacity-only — adding a transform here would persist after the animation
   finishes (animation-fill-mode: both) and break the sticky .detail-header
   inside, since any non-`none` transform on an ancestor creates a containing
   block that competes with the panel's scroll context. */
.detail {
  animation: backstage-fade-in 140ms ease-out both;
}

/* Status banner (scraper run feedback) slides down when it appears so
   the user notices it without it being abrupt. Single play on mount. */
.status-banner {
  animation: backstage-slide-in 200ms ease-out both;
}

/* Run Now loading state: button text gets replaced with a small spinner
   + "Running…" while the form is submitting. JS in app.js adds the
   `is-loading` class on submit and disables the button. */
.btn.is-loading {
  position: relative;
  color: transparent !important;
  pointer-events: none;
  transform: none;
  box-shadow: none;
}

.btn.is-loading::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 14px;
  height: 14px;
  margin: -7px 0 0 -7px;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-right-color: transparent;
  opacity: 0.85;
  color: var(--text);
  animation: backstage-spin 0.7s linear infinite;
}

/* On the white Run Now button the spinner needs to read against the
   filled background. */
.btn-primary.is-loading::after {
  border-color: #000;
  border-right-color: transparent;
}

/* ── Reduced motion ─────────────────────────────────────────────────────
   Users who've asked their OS for reduced motion get all animations and
   transforms collapsed to instant transitions. State changes still
   render — we just don't move things around to get there. The spinner
   pseudo-element stops rotating but the loading state remains visible
   via the dimmed/disabled treatment. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }

  .listing-card:hover,
  .listing-card:focus-within,
  .btn:hover,
  .btn:focus-visible,
  .btn:active,
  .listing-card-chevron,
  .listing-card:hover .listing-card-chevron {
    transform: none !important;
  }

  .btn.is-loading::after {
    animation: none !important;
  }

  /* The skeleton shimmer + scan-dot pulse are decorative. Without
     animation the dots would sit at their 0% keyframe (opacity 0.3),
     which reads as "broken" rather than "loading". Lock them at a
     readable static opacity so the loading state still looks intentional. */
  .scan-dot {
    opacity: 0.7 !important;
  }

  /* Static skeleton fill — without the shimmer sweep, the gradient
     start position renders as a uniform faint block, which is the
     correct calm-state appearance for reduced-motion users. */
}

/* ── Scrollbars (subtle) ──────────────────────────────────────────────── */

::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: rgba(255,255,255,0.08);
  border-radius: 10px;
  border: 2px solid transparent;
  background-clip: padding-box;
}

::-webkit-scrollbar-thumb:hover {
  background: rgba(255,255,255,0.16);
  background-clip: padding-box;
}

/* ── Profile form ─────────────────────────────────────────────────────────
   Inherits the .entry-form base styles (inputs, selects, textareas,
   focus ring — all consolidated above) and adds the profile-specific
   bits: the chip-style tag input, the gig-sizes checkbox group, and
   the repeating social-media rows. The previous .profile-form select /
   input[type="number"] rules were removed 2026-05-22 — they duplicated
   the .entry-form base and broke on Safari (native widget chrome was
   winning specificity over the unscoped rule). The consolidated rule
   above uses `appearance: none` to take full control. */

/* Gig-sizes checkboxes render as a wrapping row of small pills so the six
   options stay compact without forcing a vertical column. */
/* Borderless fieldset that groups a checkbox set under a <legend> caption
   (WCAG 1.3.1 — gives the group a programmatic name for assistive tech).
   Resets the UA's default border/margin/padding so it renders identically
   to the old <span> caption + div. min-width:0 defeats fieldset's default
   min-content sizing inside the flex .entry-row. */
.checkbox-fieldset {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
}

.checkbox-fieldset > legend {
  padding: 0;
  margin-bottom: 5px;
}

.checkbox-group {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.checkbox-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: rgba(0,0,0,0.25);
  color: var(--text-2);
  font-size: 13px;
  cursor: pointer;
}

.checkbox-pill:hover {
  border-color: var(--border-strong);
  color: var(--text);
}

.checkbox-pill input[type="checkbox"] {
  margin: 0;
  accent-color: var(--blue);
}

.checkbox-pill:has(input[type="checkbox"]:checked) {
  background: var(--blue-bg);
  border-color: var(--blue);
  color: var(--text);
}

/* Social media rows — repeating (platform, handle, followers, remove)
   on desktop; stacks to a column on narrow screens. */
.social-media-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.social-media-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}

.social-media-row select {
  flex: 0 0 140px;
}

.social-media-row input[type="text"] {
  flex: 1 1 auto;
  min-width: 0;
}

.social-media-row input[type="number"] {
  flex: 0 0 130px;
}

.social-media-row .social-media-remove {
  flex: 0 0 auto;
}

.social-media-actions {
  margin-top: 8px;
}

/* Errors block inside the status banner: keep the bullets but reset the
   default UA padding so they sit cleanly under the strong heading. */
.form-errors {
  margin: 4px 0 0;
  padding-left: 18px;
  font-size: 13px;
  color: var(--text);
}

.form-errors li + li {
  margin-top: 2px;
}

@media (max-width: 880px) {
  .social-media-row {
    flex-direction: column;
    align-items: stretch;
  }
  .social-media-row select,
  .social-media-row input[type="text"],
  .social-media-row input[type="number"] {
    flex: 1 1 auto;
    width: 100%;
  }
  .social-media-row .social-media-remove {
    align-self: flex-end;
  }
}

/* ── Admin dashboard ──────────────────────────────────────────────────── */

/* Admin table follows the same dark-panel rhythm as the rest of the
   dashboard. Columns are tabular-nums for date alignment; the actions
   column stretches to fit a couple of inline forms. The "you" row gets
   a subtle blue tint so the admin sees their own row at a glance and
   knows the disabled actions there are a feature, not a bug. */

.admin-count {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 8px;
  background: rgba(255, 255, 255, 0.06);
  border-radius: 999px;
  font-size: 11px;
  font-weight: 500;
  color: var(--text-2);
  letter-spacing: 0;
  vertical-align: middle;
}

.section-heading-spaced {
  margin-top: 32px;
}

.admin-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  margin-top: 12px;
}

.admin-table th,
.admin-table td {
  text-align: left;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}

/* Field-change list inside the "Recently merged" panel — one <li> per
   old → new column diff. Kept compact so a multi-field merge doesn't
   balloon the table row. */
.admin-merge-diff {
  margin: 0;
  padding-left: 16px;
  font-size: 12px;
  line-height: 1.5;
}

.admin-merge-diff code {
  font-size: 11px;
  color: var(--text-2);
}

.admin-table thead th {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  /* --text-2 (not --text-3): column headers are scanned constantly, so
     legibility wins. The uppercase + tracking + weight still read them as
     labels against the brighter --text data cells below — colour isn't the
     only thing carrying the hierarchy. */
  color: var(--text-2);
  border-bottom: 1px solid var(--border-strong);
}

.admin-table tbody tr:hover {
  background: rgba(255, 255, 255, 0.02);
}

.admin-row-self {
  background: var(--blue-bg);
}

.admin-row-self:hover {
  background: var(--blue-bg) !important;
}

/* Left accent bar on the current-session row so it reads as "this is you"
   at a glance. box-shadow (not border-left) survives border-collapse. */
.admin-row-self td:first-child {
  box-shadow: inset 3px 0 0 0 var(--blue);
}

.admin-email {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
  font-size: 12px;
  color: var(--text);
  word-break: break-all;
}

.admin-actions-col {
  width: 1%;
  white-space: nowrap;
}

.admin-actions {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.admin-action-form {
  display: inline;
  margin: 0;
}

/* Manual-actions card (/admin#manual-actions) — on-demand scraper triggers.
   Each action is a button + optional hint; wrap into a responsive row. */
.admin-manual-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: flex-start;
}
.admin-manual-action {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}
.admin-manual-hint {
  font-size: 11px;
  color: var(--text-2);
  max-width: 200px;
  line-height: 1.3;
}
.admin-manual-hint a {
  color: var(--text);
  text-decoration: underline;
}

.admin-btn {
  font-size: 12px;
  padding: 4px 10px;
}

.admin-actions-self {
  color: var(--text-3);
  font-size: 12px;
}

.admin-pill {
  display: inline-block;
  padding: 1px 8px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  margin-right: 4px;
}

.admin-pill-admin {
  background: var(--blue-bg);
  color: var(--blue);
}

.admin-pill-user {
  background: rgba(255, 255, 255, 0.05);
  color: var(--text-2);
}

/* "Current session" pill — solid blue so it clearly marks the signed-in
   admin's own row, distinct from the translucent .admin-pill-admin chip. */
.admin-pill-self {
  background: var(--blue);
  color: #06243f;
  font-weight: 600;
}

/* Self-row Actions cell: no controls (self-management is blocked), just a
   disabled-looking label + a visible reason. The note IS the explanation;
   the title attribute mirrors it for hover. */
.admin-self-cell {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
  text-align: right;
}
.admin-self-disabled {
  font-size: 12px;
  padding: 4px 10px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-1);
  color: var(--text-3);
  opacity: 0.6;
  cursor: not-allowed;
}
.admin-self-note {
  font-size: 11px;
  color: var(--text-3);
  max-width: 220px;
  line-height: 1.3;
}

/* ── Admin: Manage-user modal (native <dialog>) ─────────────────────────────
   Per-user permission changes route through a confirmation step here. Opened
   by app.js via showModal(); the ::backdrop + focus trap + Esc come for free.
   CSP-clean: no inline styles/scripts. */
.admin-manage-dialog {
  width: min(440px, calc(100vw - 32px));
  max-width: 440px;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.5);
}
.admin-manage-dialog::backdrop {
  background: rgba(0, 0, 0, 0.55);
}
.admin-manage-inner {
  padding: 20px;
}
.admin-manage-pane {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
/* The HTML `hidden` attribute must beat the pane's display:flex — same UA
   cascade trap as .status-banner[hidden] / .bulk-action-bar[hidden]. */
.admin-manage-pane[hidden] {
  display: none;
}

.admin-manage-head {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.admin-manage-title {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
}
.admin-manage-sub {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 12px;
  color: var(--text-2);
}
.admin-manage-explain {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-2);
}
.admin-manage-explain strong {
  color: var(--text);
}

.admin-manage-stats {
  display: flex;
  gap: 8px;
  margin: 0;
}
.admin-manage-stats > div {
  flex: 1;
  padding: 8px 10px;
  text-align: center;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-1);
}
.admin-manage-stats dt {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-3);
}
.admin-manage-stats dd {
  margin: 2px 0 0;
  font-size: 14px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.admin-manage-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.admin-manage-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 100%;
  padding: 10px 12px;
  text-align: left;
  text-decoration: none;
  font: inherit;
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-1);
  cursor: pointer;
  transition: background 0.14s ease, border-color 0.14s ease;
}
.admin-manage-item:hover {
  background: var(--surface-2);
  border-color: var(--border-strong);
}
.admin-manage-item:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}
.admin-manage-item-label {
  font-size: 13px;
  font-weight: 600;
}
.admin-manage-item-desc {
  font-size: 11px;
  color: var(--text-2);
}
/* Danger is a hint here (red label), not the destructive button itself —
   the solid red fill is reserved for the final Disallow confirm. */
.admin-manage-item-danger .admin-manage-item-label {
  color: var(--red);
}
.admin-manage-item-danger:hover {
  border-color: rgba(255, 107, 107, 0.35);
  background: var(--red-bg);
}

.admin-manage-foot {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 8px;
}
.admin-manage-foot .admin-action-form {
  display: inline;
  margin: 0;
}

.admin-source-form {
  display: flex;
  flex-direction: column;
  gap: 14px;
  max-width: 720px;
  margin-top: 4px;
}
.admin-source-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.admin-source-field label {
  font-size: 12px;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--text-2);
}
/* Text, number, and textarea share one boundary treatment so every admin
   field reads as the same control. The textarea rule also styles the
   no-JS fallback for the Discord channel box (when the chip controller is
   inactive the raw <textarea> shows and would otherwise be browser-default
   white). */
.admin-source-field input[type="text"],
.admin-source-field input[type="number"],
.admin-source-field textarea {
  width: 100%;
  padding: 10px 12px;
  background: rgba(0, 0, 0, 0.28);
  border: 1px solid var(--input-border);
  border-radius: 6px;
  color: var(--text);
  font-family: inherit;
  font-size: 14px;
  transition: border-color 0.14s ease, background 0.14s ease;
}
.admin-source-field textarea {
  min-height: 92px;
  line-height: 1.5;
  resize: vertical;
}
/* Hover brightens the resting boundary so the field reads as interactive
   before focus (the #767676 resting border clears 3:1 but stays quiet). */
.admin-source-field input[type="text"]:hover,
.admin-source-field input[type="number"]:hover,
.admin-source-field textarea:hover {
  border-color: var(--text-3);
}
.admin-source-field input[type="text"]:focus,
.admin-source-field input[type="number"]:focus,
.admin-source-field textarea:focus {
  outline: 2px solid rgba(120, 170, 255, 0.45);
  outline-offset: 1px;
  border-color: rgba(120, 170, 255, 0.6);
  background: rgba(0, 0, 0, 0.34);
}
/* Error: red boundary + faint red wash. Keyed on aria-invalid="true" (the
   standard a11y hook) so server- or JS-side validation can flag a field
   without inventing a bespoke class. */
.admin-source-field input[aria-invalid="true"],
.admin-source-field textarea[aria-invalid="true"] {
  border-color: var(--red);
  background: var(--red-bg);
}
/* Disabled: clearly inert (dimmed, not-allowed) but the value stays
   readable rather than fading out. */
.admin-source-field input:disabled,
.admin-source-field textarea:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  background: rgba(0, 0, 0, 0.18);
}
.admin-source-actions {
  display: flex;
  gap: 10px;
}
.admin-source-field-narrow {
  max-width: 200px;
}
/* ── Source settings matrix ────────────────────────────────────────────────
   Combined scrape-toggle + Discord-ping grid (replaces the two separate
   checkbox groups). Builds on the shared .admin-table look; the toggle cells
   center an accent checkbox, notification-only sources show an N/A pill, and
   the Last Run / Last Error / Notes columns carry muted metadata. */
.admin-matrix-form {
  max-width: none;          /* the table is wider than the 720px form cap */
}
.admin-matrix-help {
  margin: 0 0 14px;
  padding: 10px 12px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-2);
  background: rgba(120, 170, 255, 0.07);
  border: 1px solid rgba(120, 170, 255, 0.22);
  border-left: 3px solid rgba(120, 170, 255, 0.55);
  border-radius: 6px;
}
.admin-matrix-help strong {
  color: var(--text);
}
.admin-matrix-scroll {
  overflow-x: auto;          /* never let the 6-col matrix overflow the page */
  margin-bottom: 14px;
}
.admin-source-matrix {
  margin: 0;
}
.admin-source-matrix th[scope="row"].matrix-source {
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
}
.admin-source-matrix .matrix-col-toggle {
  text-align: center;
  width: 1%;
  white-space: nowrap;
}
.admin-source-matrix tbody td.matrix-col-toggle {
  vertical-align: middle;
}
/* Clickable padding around the checkbox so the whole cell is an easy target. */
.matrix-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 6px 10px;
  cursor: pointer;
}
.matrix-toggle input[type="checkbox"] {
  width: 17px;
  height: 17px;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}
.matrix-na {
  display: inline-block;
  padding: 2px 9px;
  font-size: 11px;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--text-3, var(--text-2));
  background: rgba(255, 255, 255, 0.04);
  border: 1px dashed var(--border);
  border-radius: 999px;
  cursor: help;
}
.admin-source-matrix .matrix-meta {
  font-size: 12.5px;
  color: var(--text-2);
  white-space: nowrap;
}
.admin-source-matrix .matrix-err {
  white-space: normal;
  max-width: 240px;
}
.matrix-ok {
  color: var(--text-3, var(--text-2));
}
.admin-source-matrix .matrix-note {
  font-size: 12.5px;
  color: var(--text-2);
  line-height: 1.5;
  min-width: 200px;
}
.admin-source-matrix .row-empty {
  color: var(--text-3, var(--text-2));
  opacity: 0.7;
}

/* "Last updated <date> by <admin>" audit line under a settings card's
   description. Small, muted, never blocks the controls. */
.admin-setting-meta {
  margin: 6px 0 0;
  font-size: 12px;
  color: var(--text-3, var(--text-2));
  font-style: italic;
}

/* ── Admin settings forms: save states + dirty + inline feedback ───────────
   Per-form Save → Saving… → Saved / Failed states, an "Unsaved changes"
   pill, and an inline success/error/warning message. Driven by app.js
   (gated on [data-admin-form]); the forms still POST + flash without JS. */
.admin-source-actions {
  align-items: center;
  flex-wrap: wrap;
}

/* When there are no unsaved changes the Save button is de-emphasized so a
   dirty form's button reads as the actionable one. It stays clickable (a
   re-save is harmless / idempotent) — we only dim it. */
.admin-save-btn.is-clean {
  opacity: 0.6;
}
.admin-save-btn.is-clean:hover,
.admin-save-btn.is-clean:focus-visible {
  opacity: 1;
}

/* Transient post-submit button states. Saving keeps the label visible
   (no spinner-swap) so the "Saving…" text the task asks for actually shows. */
.admin-save-btn.is-saving {
  pointer-events: none;
  opacity: 0.8;
}
.admin-save-btn.is-saved {
  border-color: rgba(103, 214, 140, 0.7) !important;
  color: var(--green) !important;
  background: rgba(103, 214, 140, 0.12) !important;
}
.admin-save-btn.is-failed {
  border-color: rgba(255, 133, 133, 0.7) !important;
  color: var(--red) !important;
  background: rgba(255, 133, 133, 0.12) !important;
}

/* "Unsaved changes" indicator next to the Save button. */
.admin-form-dirty {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--amber);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
}
.admin-form-dirty::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--amber);
  flex: none;
}
.admin-form-dirty[hidden] {
  display: none;
}

/* Inline per-form result / validation message. Neutral by default; colored
   by state class. Takes the full row width below the button on narrow cards
   (flex-basis 100%) so longer messages don't squeeze the button. */
.admin-form-status {
  flex: 1 1 100%;
  font-size: 13px;
  line-height: 1.4;
  color: var(--text-2);
  margin-top: 2px;
}
.admin-form-status:empty {
  display: none;
}
.admin-form-status.is-success {
  color: var(--green);
}
.admin-form-status.is-error {
  color: var(--red);
}
.admin-form-status.is-warn {
  color: var(--amber);
}

/* ── Admin: chip / tag inputs (Ticketmaster states + genres, Discord IDs) ──
   Progressive enhancement over the plain text input / textarea the server
   renders. The original control is kept in the form as the value-holder
   (hidden once enhanced; synced + comma/newline-serialized exactly as the
   routes expect) so FormData submit, dirty-tracking, the existing
   [data-admin-validate] checks, and the no-JS fallback all keep working.
   Driven by the [data-chip-field] controller in app.js. CSP-clean. */
.chip-enhanced [data-chip-input] {
  display: none;
}
/* Concise per-field helper line (always rendered — no-JS friendly). */
.admin-field-help {
  margin: 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--text-2);
}
.admin-field-help code {
  font-size: 11px;
  padding: 1px 5px;
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.3);
  color: var(--text-2);
}

.chip-control {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
/* The bordered box that reads as a single input; chips + entry wrap inside. */
.chip-box {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  width: 100%;
  min-height: 42px;
  padding: 7px 8px;
  background: rgba(0, 0, 0, 0.25);
  border: 1px solid var(--input-border);
  border-radius: 6px;
  cursor: text;
}
.chip-box:focus-within {
  outline: 2px solid rgba(120, 170, 255, 0.45);
  outline-offset: 1px;
  border-color: rgba(120, 170, 255, 0.6);
}
.chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 4px 3px 10px;
  background: var(--blue-bg);
  border: 1px solid rgba(95, 168, 255, 0.35);
  border-radius: 6px;
  font-size: 13px;
  line-height: 1.3;
  color: var(--text);
  max-width: 100%;
}
.chip-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 240px;
}
.chip-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  padding: 0;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: var(--text-2);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  flex: none;
}
.chip-remove:hover {
  background: var(--red-bg);
  color: var(--red);
}
.chip-remove:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 1px;
}
.chip-entry {
  flex: 1 1 90px;
  min-width: 90px;
  padding: 4px 2px;
  border: 0;
  background: transparent;
  color: var(--text);
  font-family: inherit;
  font-size: 14px;
}
.chip-entry:focus {
  outline: none;
}
/* Inline entry feedback (invalid / duplicate) — distinct from the form-level
   .admin-form-status by the Save button. */
.chip-msg {
  font-size: 12px;
  line-height: 1.4;
  color: var(--text-2);
}
.chip-msg:empty {
  display: none;
}
.chip-msg.is-error { color: var(--red); }
.chip-msg.is-warn  { color: var(--amber); }
.chip-count {
  font-size: 11px;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--text-3);
}

/* Brief highlight when a duplicate add is rejected (the existing chip flashes
   amber, then settles back to the resting blue). */
@keyframes chip-dupe-flash {
  0%   { background: var(--amber-bg); border-color: rgba(240, 169, 71, 0.7); }
  100% { background: var(--blue-bg);  border-color: rgba(95, 168, 255, 0.35); }
}
.chip.is-dupe {
  animation: chip-dupe-flash 0.9s ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .chip.is-dupe { animation: none; }
}

/* ── Admin page: sticky in-page subnav + section cards (usability pass) ────
   The Admin page scrolls inside .admin-panel — the app shell's .main is
   height:100vh / overflow:hidden, so .panel.entry-panel is the real scroll
   container. We move that panel's padding off the scroll box and onto
   .admin-content so the sticky subnav can sit flush at the scrollport top
   with no painted gap above it. Anchor jumps + smooth scroll are native/CSS;
   app.js only paints the active pill (gated on .admin-subnav). */
.entry-panel.admin-panel {
  padding: 0;
  scroll-behavior: smooth;
}

.admin-subnav {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  gap: 6px;
  overflow-x: auto;
  padding: 10px 16px;
  background: rgba(36, 36, 36, 0.94);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--border-strong);
  scrollbar-width: thin;
}

.admin-subnav-link {
  flex: 0 0 auto;
  padding: 5px 12px;
  border-radius: 999px;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text-3);
  text-decoration: none;
  white-space: nowrap;
  border: 1px solid transparent;
  transition: color 0.12s, background 0.12s, border-color 0.12s;
}
.admin-subnav-link:hover {
  color: var(--text);
  background: var(--surface-1);
}
.admin-subnav-link.is-active {
  color: var(--blue);
  background: var(--blue-bg);
  border-color: rgba(95, 168, 255, 0.35);
}
.admin-subnav-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  color: var(--text);
}

/* Padding lives here now (it left .admin-panel so the subnav can pin flush).
   Cards stack with the inherited .entry-content flex column. */
.admin-content {
  padding: 20px 32px 56px;
  gap: 16px;
}

.admin-card {
  /* Clears the sticky subnav when an anchor jump lands on this card. */
  scroll-margin-top: 64px;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px 20px 20px;
}

.admin-card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
  margin-bottom: 14px;
}

.admin-card-headings {
  min-width: 0;
}

/* Readable, normal-case card title — distinct from the all-caps
   .section-heading micro-label, which is now reserved for table headers
   and other secondary labels. */
.admin-card-title {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text);
  text-transform: none;
}

.admin-card-desc {
  margin: 5px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
  /* Reading-length prose, not a micro-label — sits at --text-2 so it stays
     comfortably legible while the --text card title above it leads. */
  color: var(--text-2);
  max-width: 76ch;
}

.admin-card-badge {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--text-2);
  background: rgba(255, 255, 255, 0.06);
  white-space: nowrap;
}
.admin-card-badge-live {
  color: var(--green);
  background: var(--green-bg);
}
.admin-card-badge-bad {
  color: var(--red);
  background: var(--red-bg);
}
.admin-card-badge-warn {
  color: var(--amber);
  background: var(--amber-bg);
}

/* Overview: a compact summary strip. Reuses .stats-grid/.stats-card so it
   reads as the same family as System Health below, just denser — it's the
   at-a-glance decision layer, while System Health is the full raw snapshot. */
.admin-overview-grid .stats-card {
  padding: 12px 14px;
}
.admin-overview-grid .stats-card-value {
  font-size: 22px;
}

/* Needs attention: short status rows. Mirrors the .status-banner colour
   language (a coloured dot + a faint left-to-right tint) so it's visually
   distinct per level without being loud. Levels: error / warning / neutral /
   success. */
.admin-attention-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.admin-attention-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-1);
  font-size: 13px;
  line-height: 1.45;
}
.admin-attention-dot {
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--text-3);
}
.admin-attention-text {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--text);
}
.admin-attention-link {
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--text-2);
  text-decoration: none;
  white-space: nowrap;
  padding: 2px 8px;
  border-radius: 4px;
}
.admin-attention-link:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.06);
  text-decoration: none;
}
.admin-attention-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  color: var(--text);
}

.admin-attention-error {
  border-color: rgba(255, 107, 107, 0.28);
  background: linear-gradient(to right, var(--red-bg), var(--surface-1) 60%);
}
.admin-attention-error .admin-attention-dot {
  background: var(--red);
  box-shadow: 0 0 0 3px rgba(255, 107, 107, 0.18);
}
.admin-attention-warning {
  border-color: rgba(240, 169, 71, 0.25);
  background: linear-gradient(to right, var(--amber-bg), var(--surface-1) 60%);
}
.admin-attention-warning .admin-attention-dot {
  background: var(--amber);
  box-shadow: 0 0 0 3px rgba(240, 169, 71, 0.18);
}
.admin-attention-neutral .admin-attention-dot {
  background: var(--text-3);
}
.admin-attention-success {
  border-color: rgba(103, 214, 140, 0.22);
  background: linear-gradient(to right, var(--green-bg), var(--surface-1) 60%);
}
.admin-attention-success .admin-attention-dot {
  background: var(--green);
  box-shadow: 0 0 0 3px rgba(103, 214, 140, 0.15);
}

/* Empty state — subtle success, never loud. */
.admin-attention-empty {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  border: 1px solid rgba(103, 214, 140, 0.22);
  border-radius: var(--radius-sm);
  background: linear-gradient(to right, var(--green-bg), var(--surface-1) 55%);
  color: var(--text-2);
  font-size: 13px;
}
.admin-attention-empty::before {
  content: "";
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--green);
  box-shadow: 0 0 0 3px rgba(103, 214, 140, 0.15);
}

/* Tables / forms already carry their own top margin; the first child inside
   a card body shouldn't double it against the card head. */
.admin-card-body > .admin-table:first-child,
.admin-card-body > .stats-grid:first-child {
  margin-top: 0;
}

/* Table-bearing card bodies are self-contained scroll boxes:
   - horizontally, so the wide tables (rate-limit ~9 col, rollup 10 col) can't
     shrink below their content width and the page never scrolls sideways;
   - vertically (max-height), so a long table scrolls *inside the card* and its
     header can pin to the top (see the sticky thead rule below).
   Scoped via :has() to table cards only, so form cards keep visible overflow
   and don't clip input focus outlines. Short tables never reach the cap, so
   they don't get a nested scrollbar — the inner scroll only engages for the
   genuinely long tables that benefit from a sticky header (e.g. scraper runs).
   The Manage dialogs in the Active-users body open via showModal() (top layer),
   so they're unaffected by this scroll container. */
.admin-card-body:has(> .admin-table) {
  overflow: auto;
  max-height: 70vh;
}

/* Sticky table headers — pin the whole thead to the top of the scrolling card
   body so the column labels stay visible while scrolling a long table. Sticking
   the thead (not individual th cells) keeps a two-row header — the rollup +
   rate-limit group/label rows — moving as a single unit, with no per-row top
   offsets to keep in sync. Opaque background (the --panel base with the card's
   translucent --surface-1 tint composited on top, i.e. the exact card-surface
   colour) hides rows scrolling beneath. box-shadow draws the header underline
   rather than border-bottom, which can drop out on a sticky cell mid-scroll
   under border-collapse. Scoped to .admin-card-body so the shared .admin-table
   styling on /stats + /effectiveness (which don't use .admin-card-body) is
   untouched. */
.admin-card-body .admin-table thead {
  position: sticky;
  top: 0;
  z-index: 2;
}
.admin-card-body .admin-table thead th {
  background: linear-gradient(var(--surface-1), var(--surface-1)), var(--panel);
}
.admin-card-body .admin-table thead th:not(.admin-th-group) {
  box-shadow: inset 0 -1px 0 var(--border-strong);
}
.admin-card-body .admin-th-group {
  box-shadow: inset 0 -1px 0 var(--border);
}

@media (prefers-reduced-motion: reduce) {
  .entry-panel.admin-panel {
    scroll-behavior: auto;
  }
}

@media (max-width: 880px) {
  .admin-table {
    font-size: 12px;
  }
  .admin-table th,
  .admin-table td {
    padding: 8px 6px;
  }
  .admin-actions {
    flex-direction: column;
    align-items: stretch;
  }
  .admin-content {
    padding: 16px 16px 48px;
  }
  .admin-card {
    padding: 16px 14px 16px;
  }
  /* Below 880px the page scrolls (the topbar scrolls away) and the subnav
     pins to the viewport top, where the fixed hamburger (40px @ left:12px,
     z-index 1001) sits — pad the links clear of it so "Health" isn't hidden. */
  .admin-subnav {
    padding-left: 60px;
  }
}

/* ── Admin: collapsible cards (density pass) ───────────────────────────────
   Low-use / debug sections are <details class="admin-card admin-card-collapsible">
   so they fold away to a one-line summary. The <summary> IS the card head:
   title + a collapsed-state summary line + the count badge + a chevron.
   Native disclosure — zero JS for open/close. app.js only auto-opens a section
   when an anchor (subnav link, "Needs attention" View link, deep-link hash)
   targets it; without JS the native fragment jump still scrolls to the
   collapsed header, so anchor navigation degrades cleanly. */
.admin-card-summary {
  display: flex;
  align-items: center;
  gap: 14px;
  cursor: pointer;
  list-style: none;            /* Firefox: drop the default triangle marker. */
}
.admin-card-summary::-webkit-details-marker { display: none; }  /* Safari/Chrome. */
.admin-card-summary::marker { content: ""; }
.admin-card-summary .admin-card-headings {
  flex: 1 1 auto;              /* Push badge + chevron to the right edge. */
}
.admin-card-summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
/* The summary doubles as the head when open; give the body breathing room. */
.admin-card-collapsible[open] > .admin-card-summary {
  margin-bottom: 14px;
}

/* One-line "what's inside" summary shown while collapsed; redundant once the
   full body is revealed, so it's hidden when open. */
.admin-card-collapsed-summary {
  display: block;
  margin: 5px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--text-2);
}
.admin-card-collapsible[open] > .admin-card-summary .admin-card-collapsed-summary {
  display: none;
}
.admin-card-collapsed-summary-warn {
  color: var(--amber);
}

/* Disclosure chevron — rotates from ▸ to ▾ on open. */
.admin-card-chevron {
  flex: 0 0 auto;
  color: var(--text-3);
  font-size: 17px;
  line-height: 1;
  transition: transform 0.15s ease;
}
.admin-card-collapsible[open] > .admin-card-summary .admin-card-chevron {
  transform: rotate(90deg);
}

/* ── Admin: inline help toggles ("What does this control?") ────────────────
   Long operational prose moves behind a compact toggle so a short lead stays
   visible by default. Native <details>; the existing .admin-card-desc lead
   line stays above it. */
.admin-help {
  margin: 8px 0 2px;
}
.admin-help-summary {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  width: -moz-fit-content;
  width: fit-content;
  cursor: pointer;
  list-style: none;
  font-size: 12px;
  font-weight: 500;
  color: var(--blue);
}
.admin-help-summary::-webkit-details-marker { display: none; }
.admin-help-summary::marker { content: ""; }
.admin-help-summary::before {
  content: "?";
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 15px;
  height: 15px;
  border: 1px solid currentColor;
  border-radius: 50%;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
}
.admin-help-summary:hover {
  text-decoration: underline;
}
.admin-help-summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}
.admin-help[open] > .admin-help-summary {
  margin-bottom: 2px;
}
.admin-help-body {
  margin: 7px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--text-2);
  max-width: 76ch;
}

/* ── Admin runbook ──────────────────────────────────────────────────────────
   Static reference page. Reuses the .admin-card shell; the prose, lists, and
   cross-link rows below are scoped so they don't touch any other admin view. */
.admin-runbook a {
  color: var(--blue);
  text-decoration: none;
}
.admin-runbook a:hover { text-decoration: underline; }

/* In-page table of contents — anchor chips that jump to each section. */
.runbook-toc {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.runbook-toc a {
  padding: 6px 12px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  font-size: 12.5px;
  color: var(--text-2);
  background: var(--surface-1);
}
.runbook-toc a:hover {
  color: var(--text);
  border-color: var(--blue);
  text-decoration: none;
}

/* Definition list: bold term, indented explanation. */
.runbook-dl { margin: 0; }
.runbook-dl dt {
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text);
  margin-top: 16px;
}
.runbook-dl dt:first-child { margin-top: 0; }
.runbook-dl dd {
  margin: 4px 0 0;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--text-2);
  max-width: 80ch;
}

.runbook-list {
  margin: 6px 0 0;
  padding-left: 18px;
  list-style: disc;
}
.runbook-list li {
  margin-top: 5px;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--text-2);
}
.runbook-list-tight li { margin-top: 2px; }
.runbook-dl strong { color: var(--text); font-weight: 600; }

/* Row of "Go to …" cross-links at the foot of each section. */
.runbook-links {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

@media (prefers-reduced-motion: reduce) {
  .admin-card-chevron {
    transition: none;
  }
}

/* ── Ratings (post-show ratings system, migration 21) ─────────────────────
   Per-listing panel lives inside the detail panel as a <details> block;
   /ratings is a standalone index page. Both render the same atomic
   .rating-grid / .rating-cell / .rating-notes primitives so a visual
   refactor only touches one place. */

.card-rate-link {
  position: relative;
  z-index: 2;
  color: var(--text-3);
  text-decoration: none;
  font-size: 12px;
  padding: 1px 4px;
  border-radius: 4px;
  transition: color 0.12s, background 0.12s;
}
.card-rate-link:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.06);
  text-decoration: none;
}
.card-rate-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  color: var(--text);
}

.ratings-section {
  margin-top: 18px;
}

.ratings-details > summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 8px 0;
}
.ratings-details > summary::-webkit-details-marker { display: none; }
.ratings-summary .section-heading {
  margin: 0;
}
.ratings-summary-hint {
  color: var(--text-3);
  font-size: 12px;
}

.ratings-preemptive-note {
  margin: 10px 0;
  padding: 10px 12px;
  border: 1px solid rgba(255, 200, 90, 0.45);
  border-radius: 6px;
  background: rgba(255, 200, 90, 0.08);
  font-size: 13px;
  line-height: 1.45;
}

.ratings-empty {
  color: var(--text-3);
  font-style: italic;
  margin: 12px 0;
}

.ratings-list {
  list-style: none;
  padding: 0;
  margin: 12px 0 18px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.rating-entry {
  border: 1px solid var(--border);
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.02);
  padding: 10px 12px;
}
.rating-entry.is-preemptive {
  border-color: rgba(255, 200, 90, 0.35);
  background: rgba(255, 200, 90, 0.04);
}

.rating-entry-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}
.rating-entry-time {
  color: var(--text-3);
  font-size: 12px;
}

.rating-badge {
  display: inline-block;
  padding: 1px 6px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}
.rating-badge-preemptive {
  background: rgba(255, 200, 90, 0.18);
  color: rgb(255, 210, 130);
  border: 1px solid rgba(255, 200, 90, 0.45);
}

.rating-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 6px 14px;
  margin: 0;
}
.rating-cell {
  display: flex;
  flex-direction: column;
}
.rating-cell dt {
  color: var(--text-3);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.rating-cell-name {
  color: var(--text-2);
  text-transform: none;
  letter-spacing: 0;
  font-weight: 500;
}
.rating-cell dd {
  margin: 0;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
}

.rating-notes {
  margin: 8px 0 0;
  font-size: 13px;
  color: var(--text-2);
  white-space: pre-wrap;
}

/* Add/edit form on the detail panel ratings section. */
.rating-form {
  margin-top: 14px;
  padding: 12px;
  border: 1px dashed var(--border);
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.015);
}
.rating-form-title {
  margin: 0 0 4px;
  font-size: 14px;
  font-weight: 600;
}
.rating-form-hint {
  margin: 0 0 12px;
  color: var(--text-3);
  font-size: 12px;
}

.rating-form-grid {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 12px;
}
.rating-form-row {
  display: grid;
  grid-template-columns: 140px 1fr 90px;
  gap: 8px;
  align-items: center;
}
.rating-form-label {
  font-size: 13px;
  color: var(--text-2);
}
.rating-form-name {
  padding: 4px 8px;
  font-size: 13px;
  background: rgba(0, 0, 0, 0.2);
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text);
}
.rating-form-name-static {
  display: block;
  /* Empty placeholder cell so the grid stays aligned. */
}
.rating-form-score {
  padding: 4px 8px;
  font-size: 13px;
  background: rgba(0, 0, 0, 0.2);
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text);
}

.rating-form-notes {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 12px;
}
.rating-form-notes textarea {
  width: 100%;
  padding: 8px 10px;
  font-size: 13px;
  background: rgba(0, 0, 0, 0.2);
  border: 1px solid var(--input-border);
  border-radius: 4px;
  color: var(--text);
  resize: vertical;
  font-family: inherit;
}

.rating-form-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}
.rating-form-meta {
  color: var(--text-3);
  font-size: 12px;
}

.ratings-signin-cta {
  color: var(--text-3);
  font-size: 13px;
  margin-top: 12px;
}

/* /ratings index page. */
.ratings-search-form {
  display: flex;
  gap: 8px;
  align-items: center;
  flex: 1 1 auto;
  max-width: 540px;
}
.ratings-search-form input[type="text"] {
  flex: 1 1 auto;
  padding: 6px 10px;
  font-size: 13px;
  background: rgba(0, 0, 0, 0.2);
  border: 1px solid var(--input-border);
  border-radius: 4px;
  color: var(--text);
}

.ratings-index {
  padding: 20px 24px 40px;
  max-width: 960px;
  margin: 0 auto;
}
.ratings-index-hint {
  color: var(--text-3);
  font-size: 13px;
  margin: 0 0 18px;
}
.ratings-index-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.ratings-index-entry {
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel);
  padding: 14px 16px;
}
.ratings-index-entry.is-preemptive {
  border-color: rgba(255, 200, 90, 0.35);
}
.ratings-index-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 10px;
}
.ratings-index-title {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
}
.ratings-index-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  color: var(--text-3);
  font-size: 12px;
  margin-left: auto;
}
.ratings-index-link {
  color: var(--accent);
  text-decoration: none;
}
.ratings-index-link:hover {
  text-decoration: underline;
}

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

@media (max-width: 880px) {
  .rating-form-row {
    grid-template-columns: 1fr;
  }
  .ratings-index-meta {
    margin-left: 0;
    width: 100%;
  }
}

/* ── Reputation page (aggregated ratings by entity) ─────────────────────── */

/* Sub-nav item — indented under its parent .nav-item. Sized between the
   regular .nav-item and the smaller .nav-substat so the hierarchy reads
   as "primary > secondary > footnote". */
.nav-sublink {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 12px 4px 28px;
  color: var(--text-3);
  text-decoration: none;
  font-size: 13px;
  border-radius: 4px;
  margin-top: 1px;
  transition: color 0.12s, background 0.12s;
}
.nav-sublink:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.04);
}
.nav-sublink.active {
  color: var(--text);
  background: rgba(255, 255, 255, 0.06);
}
.nav-sublink-arrow {
  color: var(--text-3);
  font-size: 11px;
  width: 12px;
  display: inline-block;
}

.reputation-page {
  padding: 20px 24px 40px;
  max-width: 960px;
  margin: 0 auto;
}
.reputation-hint {
  color: var(--text-3);
  font-size: 13px;
  margin: 0 0 18px;
}
.reputation-hint a {
  color: var(--accent);
  text-decoration: none;
}
.reputation-hint a:hover { text-decoration: underline; }

/* Scope toggle group (Community / My ratings only) sits in the topbar
   next to the category-tabs. Visually mirrors the category-tabs strip
   but with its own group label via aria-label. */
.reputation-scope {
  margin-left: 12px;
}

/* Shared by the reputation category tabs AND the board topbar's pipeline
   tabs (both use .tab) — a global rule on purpose, don't scope it. */
.tab-count {
  color: var(--text-3);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  margin-left: 2px;
}
.tab.active .tab-count {
  color: inherit;
  opacity: 0.7;
}

.reputation-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.reputation-entry {
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel);
  overflow: hidden;
}

.reputation-details > summary {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto auto;
  align-items: center;
  gap: 14px;
  padding: 12px 16px;
  cursor: pointer;
  list-style: none;
}
.reputation-details > summary::-webkit-details-marker { display: none; }
.reputation-details[open] > summary {
  border-bottom: 1px solid var(--border);
}

.reputation-name {
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.reputation-score {
  font-size: 14px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  padding: 3px 10px;
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.04);
  /* score-{green,amber,red,none} provides the color band — same scheme
     as the listing detail's bookability badges so the visual language
     stays consistent across surfaces. */
}

.reputation-count {
  color: var(--text-3);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.reputation-disclosure {
  color: var(--text-3);
  font-size: 12px;
  transition: transform 0.15s;
}
.reputation-details[open] .reputation-disclosure {
  transform: rotate(-180deg);
}

.reputation-drill {
  padding: 12px 16px 14px;
  background: rgba(0, 0, 0, 0.12);
}

.reputation-rating-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.reputation-rating {
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.02);
}
.reputation-rating.is-preemptive {
  border-color: rgba(255, 200, 90, 0.35);
  background: rgba(255, 200, 90, 0.04);
}

.reputation-rating-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 4px;
}
.reputation-rating-score {
  font-weight: 600;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}
.reputation-rating-time {
  color: var(--text-3);
  font-size: 12px;
}

@media (max-width: 880px) {
  .reputation-scope { margin-left: 0; }
  .reputation-details > summary {
    grid-template-columns: 1fr auto;
    grid-template-areas:
      "name disclosure"
      "score count";
    row-gap: 4px;
  }
  .reputation-name      { grid-area: name; }
  .reputation-score     { grid-area: score; justify-self: start; }
  .reputation-count     { grid-area: count; justify-self: end; }
  .reputation-disclosure { grid-area: disclosure; }
}

/* ────────────────────────────────────────────────────────────────────────
   User management v1 (2026-05-21)

   Styles added for: split form rows in /profile and /settings, the /stats
   card grid (reused by /admin's system health section), the avatar +
   user-name block in the sidebar footer, and the admin per-user-rollup
   table's two-row grouped header.
   ──────────────────────────────────────────────────────────────────── */

/* Side-by-side two-input row (e.g. city + state, sort + direction).
   Falls back to a stack at narrow widths so each input keeps its
   full-width target on mobile. */
.entry-row-split {
  display: flex;
  gap: 12px;
  align-items: flex-end;
  flex-wrap: wrap;
}

.entry-row-split-half {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* /stats and /admin's system-health card share this grid. min content
   width 140px so labels never wrap awkwardly under their numbers; the
   grid expands to fill the panel width. */
.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 10px;
  margin: 8px 0 16px;
}

.stats-card {
  background: var(--panel-2, #2f2f2f);
  border: 1px solid var(--border, #3a3a3a);
  border-radius: 8px;
  padding: 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.stats-card-value {
  font-size: 22px;
  font-weight: 600;
  color: var(--text, #fff);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.stats-card-label {
  font-size: 11px;
  color: var(--text-3, #9a9a9a);
  letter-spacing: var(--tracking-caps, 0.06em);
  text-transform: uppercase;
}

.stats-card-good { border-left: 3px solid #4ec9b0; }
.stats-card-bad  { border-left: 3px solid #e0524a; }
.stats-card-warn { border-left: 3px solid var(--amber); }

/* Small muted context line under a stats-card value (e.g. "12 of 47
   replied" on /effectiveness). Sits below .stats-card-label. */
.stats-card-sub {
  font-size: 11px;
  color: var(--text-3, #9a9a9a);
  font-variant-numeric: tabular-nums;
  line-height: 1.3;
}

/* Horizontal-scroll wrapper for wide tables on narrow viewports so the
   .effectiveness-table (7 columns) doesn't clip under the page's
   overflow-x:hidden instead of scrolling. */
.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* /effectiveness breakdown table — right-align the numeric columns and
   keep them tabular so rates/counts line up; truncate long venue names. */
.effectiveness-table th.num,
.effectiveness-table td.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.effectiveness-name {
  display: inline-block;
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}

/* Sub-heading inside a /stats section — slightly smaller than
   .section-heading, less prominent than a top-of-page h2. */
.section-subheading {
  font-size: 13px;
  letter-spacing: var(--tracking-caps, 0.06em);
  text-transform: uppercase;
  color: var(--text-3, #9a9a9a);
  margin: 14px 0 6px;
}

/* Sidebar footer: avatar + name horizontal block. Wraps the existing
   .sidebar-user-name so the truncation behavior still works when an
   avatar is present. */
.sidebar-user {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 4px;
}

.sidebar-user-avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--panel-2, #2f2f2f);
  border: 1px solid var(--border, #3a3a3a);
  flex: 0 0 auto;
}

/* Per-user rollup table on /admin — slightly tighter padding than the
   default .admin-table since it has 10 columns. The two-row header uses
   .admin-th-group as the group-spanning cell with a faint baseline so
   the visual grouping reads at a glance. */
.admin-table-rollup th,
.admin-table-rollup td {
  padding: 6px 8px;
  font-variant-numeric: tabular-nums;
}

.admin-th-group {
  text-align: center;
  font-size: 11px;
  letter-spacing: var(--tracking-caps, 0.06em);
  text-transform: uppercase;
  color: var(--text-2, #b4b4b4);
  border-bottom: 1px solid var(--border, #3a3a3a);
}

.admin-row-error td {
  background: rgba(224, 82, 74, 0.08);
}

/* ── Scraper visibility (/admin) ──────────────────────────────────────────
   Per-source health cards above the Recent scraper runs table + status
   badges shared between the cards and the table. Dark-theme native: reuses
   the --green / --amber / --red / -bg tokens already used by
   .admin-card-badge, so it sits in the same visual family. */
.scraper-source-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 12px;
  margin-top: 4px;
}

.scraper-source-card {
  background: var(--panel-2, #2f2f2f);
  border: 1px solid var(--border, #3a3a3a);
  border-left: 3px solid var(--border-strong, rgba(255, 255, 255, 0.13));
  border-radius: 8px;
  padding: 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 9px;
}
/* Left accent keys off the source's current status verdict. */
.scraper-source-card.scrape-accent-success { border-left-color: var(--green); }
.scraper-source-card.scrape-accent-warning { border-left-color: var(--amber); }
.scraper-source-card.scrape-accent-failed  { border-left-color: var(--red); }
.scraper-source-card.scrape-accent-running { border-left-color: var(--blue); }

.scraper-source-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.scraper-source-name {
  font-size: 14px;
  font-weight: 600;
  color: var(--text, #fff);
}

.scraper-source-status {
  display: flex;
  align-items: center;
  gap: 8px;
}

.scraper-source-errcount {
  font-size: 11.5px;
  color: var(--red);
  font-variant-numeric: tabular-nums;
}

.scraper-source-meta {
  margin: 0;
  display: grid;
  gap: 4px;
}
.scraper-source-meta > div {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  /* 11px (was 12) + a tight gap so the label and the full ET timestamp fit on
     one line at the 4-across card width — without it "Last success" + the long
     "Jun 21, 2026 · 10:35 PM ET" wrapped both the label and the date. */
  font-size: 11px;
}
.scraper-source-meta dt {
  color: var(--text-3, #9a9a9a);
  white-space: nowrap;     /* keep "Last success" on one line, never "Last" / "success" */
}
.scraper-source-meta dd {
  margin: 0;
  color: var(--text-2, #b4b4b4);
  text-align: right;
  white-space: nowrap;     /* keep the date + time on one line, never break "ET" off */
  font-variant-numeric: tabular-nums;
}

.scraper-source-err {
  margin: 0;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--red);
  background: var(--red-bg);
  border-radius: 6px;
  padding: 6px 8px;
  word-break: break-word;
}

/* Enabled / disabled pill on each source card. */
.scrape-pill {
  flex: 0 0 auto;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: var(--tracking-caps, 0.06em);
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: 999px;
}
.scrape-pill-on  { color: var(--green); background: var(--green-bg); }
.scrape-pill-off { color: var(--text-3, #9a9a9a); background: rgba(255, 255, 255, 0.06); }

/* Status badge — shared by source cards + the runs table. Four verdicts,
   each with a leading dot so the colour reads even at small sizes. */
.scrape-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 2px 9px;
  border-radius: 999px;
  white-space: nowrap;
}
.scrape-badge::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: 0 0 auto;
}
.scrape-badge-success { color: var(--green); background: var(--green-bg); }
.scrape-badge-warning { color: var(--amber); background: var(--amber-bg); }
.scrape-badge-failed  { color: var(--red);   background: var(--red-bg); }
.scrape-badge-running { color: var(--blue);  background: var(--blue-bg); }
.scrape-badge-unknown { color: var(--text-3, #9a9a9a); background: rgba(255, 255, 255, 0.06); }

/* ── Manual-run lock UI (migration 31) ──────────────────────────────────────
   The "Currently running" callout + per-button blocked states on
   /admin/scrapers. Reuses the .scrape-badge palette + admin token colours. */
.admin-flash-link {
  margin: -4px 0 12px;
  font-size: 13px;
}
.admin-flash-link a { color: var(--blue); }

.admin-lock-panel {
  border: 1px solid var(--border, #3a3a3a);
  border-left: 3px solid var(--blue);
  background: var(--blue-bg);
  border-radius: 8px;
  padding: 10px 14px;
  margin-bottom: 14px;
}
.admin-lock-panel-head {
  margin: 0 0 6px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: var(--tracking-caps, 0.06em);
  text-transform: uppercase;
  color: var(--text-2, #b4b4b4);
}
.admin-lock-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 8px;
}
.admin-lock-item {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text, #fff);
}
.admin-lock-item-stale { /* amber-leaning border via the badge; text stays readable */ }
.admin-lock-text { color: var(--text-2, #b4b4b4); }
.admin-lock-text strong { color: var(--text, #fff); font-weight: 600; }
.admin-lock-actions {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  margin-left: auto;
}
.admin-lock-detail { color: var(--blue); font-size: 12.5px; white-space: nowrap; }
.admin-lock-clear-form { display: inline; margin: 0; }
.admin-lock-clear {
  color: var(--red);
  font-size: 12.5px;
  white-space: nowrap;
}
.admin-lock-hint { color: var(--text-3, #9a9a9a); }

.admin-manual-foot {
  margin: 12px 0 0;
  font-size: 12px;
  color: var(--text-3, #9a9a9a);
  line-height: 1.5;
}

/* Right-align numeric columns in the runs table (saved / updated / errors). */
.admin-table th.num,
.admin-table td.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Readable source name in the runs table (replaces the old monospace
   <code> rendering of the raw key). */
.scrape-source-name {
  color: var(--text, #fff);
}

/* "summary" tag marking the umbrella `all` row in the chronological log. */
.scrape-tag {
  display: inline-block;
  margin-left: 6px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: var(--tracking-caps, 0.06em);
  text-transform: uppercase;
  color: var(--text-3, #9a9a9a);
  background: rgba(255, 255, 255, 0.06);
  border-radius: 4px;
  padding: 1px 6px;
  vertical-align: middle;
}

/* ── Environment & integration checks (/admin #integrations) ─────────────
   A grouped checklist reusing the shared .admin-table + .scrape-badge-*
   palette. The <h3> labels each category; the Details cell stacks a muted
   description over an emphasized note line. */
.admin-integration-group {
  margin: 20px 0 8px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: var(--tracking-caps, 0.06em);
  text-transform: uppercase;
  color: var(--text-2, #c9c9c9);
}
.admin-card-body > .admin-integration-group:first-child { margin-top: 0; }

.admin-integration-table td { vertical-align: top; }

.admin-integration-name { white-space: normal; }
.admin-integration-label {
  font-weight: 600;
  color: var(--text, #f0f0f0);
}
.admin-integration-env {
  display: inline-block;
  margin-left: 6px;
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 11px;
  color: var(--text-3, #9a9a9a);
  background: rgba(255, 255, 255, 0.05);
  border-radius: 4px;
  padding: 1px 5px;
  word-break: break-all;
}
.admin-integration-details {
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.admin-integration-desc { color: var(--text-2, #c9c9c9); }
.admin-integration-note {
  color: var(--text-3, #9a9a9a);
  font-size: 12px;
}

/* ── Production Safety Checklist (/admin/config #checklist) ───────────────
   Reuses the .admin-integration-table layout. The checklist labels are full
   item names (not just var names), so the first column wants a little more
   room and the status column shouldn't collapse. The granular env-detail
   table below it (collapsed by default) keeps the default widths. */
.admin-checklist-table th:first-child,
.admin-checklist-table td:first-child { width: 30%; }
.admin-checklist-table th:nth-child(2),
.admin-checklist-table td:nth-child(2) { width: 116px; white-space: nowrap; }

/* Umbrella rows get a faint wash + blue left accent so the per-invocation
   summary reads apart from its per-source breakdown — without reordering
   the chronological log. .admin-row-error td (red) intentionally wins on
   error rows. */
.admin-row-umbrella {
  background: rgba(255, 255, 255, 0.025);
}
.admin-row-umbrella td:first-child {
  box-shadow: inset 2px 0 0 var(--blue, #5fa8ff);
}

/* Inline error-detail expander in the runs table's Errors cell. Zero-JS
   native <details>; the summary IS the red "N errors" badge with a caret cue,
   and the message list drops left-aligned below. */
.scrape-errdetails {
  display: inline-block;
}
.scrape-errdetails > summary {
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
.scrape-errdetails > summary::-webkit-details-marker { display: none; }
/* Caret affordance on the badge so it reads as expandable. */
.scrape-errdetails > summary::after {
  content: "\25BE"; /* ▾ */
  font-size: 9px;
  color: var(--text-3);
  line-height: 1;
}
.scrape-errdetails[open] > summary::after {
  content: "\25B4"; /* ▴ */
}
.scrape-errlist {
  margin: 6px 0 0;
  padding-left: 16px;
  text-align: left;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--text-2, #b4b4b4);
  white-space: normal;
  max-width: 340px;
}

/* Inline status badges for numeric cells (errors count, etc.). Keep the raw
   number inside the badge so the colour enhances — never replaces — the value.
   Reuses the same colour tokens as .admin-card-badge / .scrape-badge. */
.admin-num-badge {
  display: inline-block;
  padding: 1px 9px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
}
.admin-num-badge-ok {
  color: var(--text-3);
  background: rgba(255, 255, 255, 0.05);
}
.admin-num-badge-bad {
  color: var(--red);
  background: var(--red-bg);
}
.scrape-errlist li {
  word-break: break-word;
}

/* ─────────────────────────────────────────────────────────────────────
   Scraper-run detail view (GET /admin/scrapers/runs/:id) — the
   breadcrumb, the per-run definition grid, the safe error list, and the
   runs-table per-row action link. Reuses the .admin-card / .scrape-badge /
   .admin-num-badge tokens above; only the detail-specific bits live here.
   ───────────────────────────────────────────────────────────────────── */

/* Breadcrumb above the run-detail cards (Admin / Scrapers / <Source> run). */
.admin-breadcrumb {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  font-size: 12.5px;
  color: var(--text-3);
}
.admin-breadcrumb a {
  color: var(--text-2);
  text-decoration: none;
}
.admin-breadcrumb a:hover {
  color: var(--text);
  text-decoration: underline;
}
.admin-breadcrumb-sep { color: var(--text-3); }
.admin-breadcrumb-current {
  color: var(--text);
  font-weight: 600;
}

/* Per-row action link in the Recent scraper runs table's Details column. */
.admin-run-action {
  font-size: 12px;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}
.admin-run-action:hover { text-decoration: underline; }
.admin-run-action-error { color: var(--red); }
.admin-run-action-muted { color: var(--text-3); }

/* Definition grid of run fields on the detail page. auto-fit so it reflows
   from one column (mobile) up to ~4 on a wide monitor without media queries. */
.scrape-detail-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 14px 20px;
  margin: 0;
}
.scrape-detail-item { min-width: 0; }
.scrape-detail-item dt {
  margin: 0 0 4px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--text-3);
}
.scrape-detail-item dd {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.4;
  color: var(--text);
  word-break: break-word;
}
.scrape-detail-num { font-variant-numeric: tabular-nums; }
.scrape-detail-raw { color: var(--text-3); font-size: 12px; }
.scrape-detail-muted { color: var(--text-3); font-size: 12.5px; }

/* Safe (secret-redacted) error message list on the detail page. */
.scrape-detail-errlist {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.scrape-detail-errlist li {
  padding: 10px 12px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border);
  border-left: 2px solid var(--red);
  border-radius: var(--radius-sm);
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-2);
  word-break: break-word;
}

/* Back-link below the detail cards. */
.admin-detail-footer { margin-top: 2px; }

/* Settings → Default board view section spacing — keeps the visual
   rhythm of the existing .thresholds block. */
.board-defaults {
  margin-top: 28px;
}

.board-defaults-form {
  display: flex;
  flex-direction: column;
  gap: 14px;
  max-width: 560px;
}

/* Avatar lives in its own .form-section above the main profile <form>
   (HTML doesn't allow nested forms — the upload + remove <form> tags
   inside the main form were implicitly closing it and orphaning every
   textarea/select after). The section gets the same panel-style border
   and padding as .entry-form so the two read as related cards. */
.avatar-section {
  padding: 18px 24px 22px;
  background: rgba(255, 255, 255, 0.02);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  border-bottom: 1px solid var(--border); /* override the .form-section :last-of-type reset */
}

.avatar-section .section-heading {
  margin: 0 0 12px;
}

/* Avatar control on /profile: preview tile + the two action forms
   (upload, remove). The preview reserves a fixed square so the layout
   doesn't jump between "no avatar" and "uploaded" states. */
.avatar-control {
  display: flex;
  gap: 14px;
  align-items: flex-start;
  flex-wrap: wrap;
}

.avatar-preview-wrap {
  flex: 0 0 auto;
  width: 64px;
  height: 64px;
  border-radius: 8px;
  overflow: hidden;
  background: var(--panel-2, #2f2f2f);
  border: 1px solid var(--border, #3a3a3a);
  display: flex;
  align-items: center;
  justify-content: center;
}

.avatar-preview {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.avatar-empty {
  font-size: 11px;
  color: var(--text-3, #9a9a9a);
  letter-spacing: var(--tracking-caps, 0.06em);
  text-transform: uppercase;
  text-align: center;
  padding: 0 6px;
  line-height: 1.2;
}

.avatar-control-actions {
  flex: 1 1 auto;
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
}

.avatar-upload-form,
.avatar-remove-form {
  display: inline-flex;
  gap: 8px;
  align-items: center;
  margin: 0;
}

/* File-input dark-mode parity. Browser-default file inputs render with
   a light background + dark text; without these rules the input would
   read as a white slab on the dark form. */
.avatar-upload-form input[type="file"] {
  color: var(--text, #fff);
  font: inherit;
  font-size: 13px;
  max-width: 280px;
}

.avatar-upload-form input[type="file"]::file-selector-button {
  background: rgba(0, 0, 0, 0.25);
  color: var(--text, #fff);
  border: 1px solid var(--border, #3a3a3a);
  border-radius: var(--radius-sm, 6px);
  padding: 6px 12px;
  font: inherit;
  font-size: 12px;
  cursor: pointer;
  transition: background-color 0.14s ease;
  margin-right: 10px;
}

.avatar-upload-form input[type="file"]::file-selector-button:hover {
  background: rgba(0, 0, 0, 0.40);
}

/* ── Login + connected accounts (2026-05-23) ────────────────────────────── */
/* Login page is the only place that wants a constrained-width card —
   every other entry-panel page (profile / admin / stats) benefits from
   the wide layout. Constrain via a per-panel class rather than touching
   .entry-panel's `flex: 1` rule. */
.login-panel {
  max-width: 480px;
  margin: 0 auto;
}

.auth-stack {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-top: 8px;
}

.auth-stack .auth-form {
  margin: 0;
}

.auth-button {
  width: 100%;
  justify-content: center;
}

.auth-legal {
  margin-top: 18px;
  font-size: 12px;
  color: var(--text-muted);
  text-align: center;
  line-height: 1.5;
}

.auth-legal a {
  color: var(--text-muted);
  text-decoration: underline;
}

.auth-legal a:hover {
  color: var(--text);
}

/* Connected accounts section on /profile. Renders one card per provider
   listing connection state + a Connect or Disconnect form. */
.providers-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.provider-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.02);
}

.provider-row.is-connected {
  border-color: rgba(80, 200, 120, 0.30);
  background: rgba(80, 200, 120, 0.06);
}

.provider-info {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.provider-name {
  font-weight: 600;
}

.provider-status {
  font-size: 12px;
  color: var(--text-2);
}

.provider-status.is-connected {
  color: rgb(120, 220, 150);
}

.provider-action-form {
  margin: 0;
  flex-shrink: 0;
}

/* ── 429 rate-limit page ───────────────────────────────────────────────── */
/* Self-contained page rendered with layout: false so we don't pay for the
   sidebar's Supabase round-trips on every throttled request. Visually it
   matches the dashboard's dark surface palette so a real user landing
   here knows they're still on Backstage, not a generic Rack error page. */
.rate-limited-body {
  margin: 0;
  min-height: 100vh;
  background: var(--bg);
  color: var(--text-1);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: inherit;
}

.rate-limited-page {
  width: 100%;
  display: flex;
  justify-content: center;
  padding: 32px 24px;
}

.rate-limited-card {
  max-width: 480px;
  width: 100%;
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  padding: 40px 32px;
  text-align: center;
  box-shadow: 0 10px 32px rgba(0, 0, 0, 0.35);
}

.rate-limited-glyph {
  font-size: 40px;
  line-height: 1;
  margin-bottom: 16px;
}

.rate-limited-title {
  margin: 0 0 12px;
  font-size: 22px;
  font-weight: 600;
}

.rate-limited-message {
  margin: 0 0 16px;
  color: var(--text-2);
}

.rate-limited-retry {
  margin: 0 0 20px;
  font-size: 16px;
}

.rate-limited-retry strong {
  font-variant-numeric: tabular-nums;
  color: var(--text-1);
}

.rate-limited-hint {
  margin: 0 0 24px;
  font-size: 13px;
  color: var(--text-3);
  line-height: 1.5;
}

.rate-limited-actions {
  margin: 0;
}

/* ── /admin rate-limit panel ───────────────────────────────────────────── */
/* Per-user × per-bucket table. Reuses .admin-table styling for the row
   chrome; adds a meter element rendered inline with the count so a high
   utilization is glanceable. */
.rate-limit-cell {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Qualify with .admin-table td so this beats the base `.admin-table td`
   left-align (equal class count, so the bare `.rate-limit-cell` lost). */
.admin-table td.rate-limit-cell {
  text-align: right;
}

/* Utilization tiers (count / limit): neutral < 70%, warning 70–89%, danger
   90%+ . The raw "count / limit" stays visible — the colour is the status
   signal, kept subtle because every (user × bucket) cell carries one. */
.rate-limit-cell.is-near {
  color: var(--amber);
}

.rate-limit-cell.is-tripped {
  color: var(--red);
  font-weight: 600;
}

.rate-limit-reset {
  font-size: 11px;
  color: var(--text-3);
  margin-left: 4px;
}

/* Legal links (privacy + terms) — grouped under a divider so they read as
   a distinct section from the account block (name + sign out) above.
   margin-top is 4px (not 8px) because .sidebar-footer's own `gap: 4px`
   already contributes 4px above this element — 4 + 4 = 8px above the line,
   matched by 8px padding below it so the divider sits visually centered.
   The -15px side margins cancel .sidebar-footer's 15px horizontal padding
   so the divider runs edge-to-edge like the footer's own top border; the
   matching 15px side padding pulls the link text back to its original
   inset. */
.sidebar-legal {
  margin-top: 4px;
  margin-left: -15px;
  margin-right: -15px;
  padding-top: 8px;
  padding-left: 15px;
  padding-right: 15px;
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 5px;
}

/* Legal links in sidebar footer. No `opacity` — at 0.6 the effective
   color was #6a6a6a = 3.41:1 over the --sidebar #141414 background, below
   the WCAG AA 4.5:1 minimum. Bumped --text-3 → --text-2 (~8.8:1 over the
   sidebar) so the small uppercase links read easily, still clearly muted
   relative to --text. */
.sidebar-privacy-link {
  display: block;
  font-size: 10.5px;
  color: var(--text-2);
  text-decoration: none;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  transition: color 0.14s ease;
}

.sidebar-privacy-link:hover {
  color: var(--text);
}

/* ── Privacy policy page ──────────────────────────────────────────────── */

.privacy-panel {
  flex: 1;
  min-width: 0;
  max-width: 720px;
  padding: 28px 32px 36px;
}

.privacy-body {
  display: flex;
  flex-direction: column;
  gap: 28px;
}

.privacy-section h2 {
  font-size: 11.5px;
  font-weight: 600;
  color: var(--text);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  margin: 0 0 10px;
}

.privacy-section p,
.privacy-section ul {
  font-size: 13.5px;
  color: var(--text-2);
  line-height: 1.65;
  margin: 0;
}

.privacy-section ul {
  padding-left: 20px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.privacy-section strong {
  color: var(--text);
  font-weight: 500;
}

.privacy-section a {
  color: var(--blue);
  text-decoration: none;
}

.privacy-section a:hover {
  text-decoration: underline;
}

.privacy-back {
  padding-top: 12px;
}


/* ── Landing page ───────────────────────────────────────────────────────
   Standalone marketing page rendered at /landing with layout:false.
   No sidebar; the body opts out of the flex layout the dashboard uses
   so the page reads as a long-scroll document. Inherits all design
   tokens from :root — palette, radii, text scale — so the page reads
   as the same product without the chrome. */

.landing-body {
  display: block;
  background: var(--bg);
  color: var(--text);
  min-height: 100vh;
  overflow-x: hidden;
}

/* Fixed minimal header. Backdrop-filter blur is progressive enhancement
   — falls back to a solid panel tint where unsupported. */
.landing-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 32px;
  background: rgba(20, 20, 20, 0.55);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border);
  transition: background 0.2s ease, border-color 0.2s ease;
}

.landing-brand {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  text-decoration: none;
  color: var(--text);
}

.landing-brand-name {
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text);
}

.landing-brand-sub {
  font-size: 11px;
  color: var(--text-3);
  letter-spacing: 0;
}

.landing-header-nav {
  display: inline-flex;
  align-items: center;
  gap: 18px;
}

.landing-header-link {
  font-size: 13px;
  font-weight: 500;
  color: var(--text-2);
  text-decoration: none;
  padding: 6px 14px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.03);
  transition: background 0.16s ease, border-color 0.16s ease, color 0.16s ease;
}

.landing-header-link:hover {
  background: rgba(255, 255, 255, 0.08);
  border-color: rgba(255, 255, 255, 0.22);
  color: var(--text);
}

.landing-header-link:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}

/* Secondary entrypoint next to the primary "Get early access" CTA — quieter
   (no fill / border) so it reads as a returning-user affordance, not a
   competing call to action. */
.landing-header-login {
  border-color: transparent;
  background: transparent;
}

.landing-header-login:hover {
  background: rgba(255, 255, 255, 0.06);
  border-color: var(--border-strong);
}

.landing-main {
  padding-top: 0;
}

/* Generic section wrapper. .landing-section-inner caps content width so
   the long-scroll has a calm rhythm regardless of viewport. */
.landing-section {
  padding: 96px 32px;
  position: relative;
}

.landing-section-inner {
  max-width: 1080px;
  margin: 0 auto;
}

.landing-section-inner-narrow {
  max-width: 560px;
  margin: 0 auto;
}

.landing-section-title {
  font-size: 40px;
  line-height: 1.1;
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--text);
  margin: 0 0 14px;
  text-wrap: balance;
}

.landing-section-sub {
  font-size: 17px;
  color: var(--text-2);
  margin: 0 0 48px;
  line-height: 1.5;
  max-width: 560px;
  text-wrap: balance;
}

/* ── Hero ──────────────────────────────────────────────────────────── */

.landing-hero {
  position: relative;
  min-height: 100vh;
  padding: 120px 32px 80px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  isolation: isolate;
}

/* Soft floating gradient orb behind the hero text. Pure CSS, no image
   asset. Anchored center-back with a slow ambient float so it reads
   alive without feeling like decoration. */
.landing-hero-orb {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 760px;
  height: 760px;
  max-width: 110vw;
  max-height: 110vw;
  border-radius: 50%;
  background: radial-gradient(
    closest-side,
    rgba(95, 168, 255, 0.18),
    rgba(95, 168, 255, 0.06) 45%,
    transparent 75%
  );
  filter: blur(40px);
  transform: translate(-50%, -50%);
  z-index: -1;
  animation: landing-orb-float 14s ease-in-out infinite;
  will-change: transform, opacity;
}

@keyframes landing-orb-float {
  0%, 100% {
    transform: translate(-50%, -50%) scale(1);
    opacity: 0.9;
  }
  50% {
    transform: translate(-48%, -52%) scale(1.06);
    opacity: 1;
  }
}

.landing-hero-inner {
  position: relative;
  z-index: 1;
  max-width: 880px;
  text-align: center;
}

.landing-eyebrow {
  display: inline-block;
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-3);
  padding: 6px 12px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.03);
  margin: 0 0 28px;
}

.landing-hero-title {
  font-size: clamp(40px, 6.4vw, 76px);
  line-height: 1.04;
  font-weight: 600;
  letter-spacing: -0.025em;
  color: var(--text);
  margin: 0 0 24px;
  text-wrap: balance;
}

.landing-hero-accent {
  background: linear-gradient(135deg, #fff 0%, #9fc3ff 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}

.landing-hero-sub {
  font-size: clamp(16px, 1.6vw, 19px);
  line-height: 1.55;
  color: var(--text-2);
  max-width: 620px;
  margin: 0 auto 40px;
  text-wrap: balance;
}

.landing-hero-cta {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  justify-content: center;
}

/* Primary CTA — solid white, the same accent the rest of the dashboard
   uses for "main safe action" buttons. */
.landing-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 13px 26px;
  font-size: 15px;
  font-weight: 600;
  color: #000;
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 999px;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.16s ease, border-color 0.16s ease,
              box-shadow 0.2s ease, transform 0.1s ease;
}

.landing-cta:hover {
  background: #f4f4f4;
  border-color: #f4f4f4;
  box-shadow: 0 6px 28px rgba(255, 255, 255, 0.18);
  transform: translateY(-1px);
}

.landing-cta:active {
  transform: translateY(0);
}

.landing-cta:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 3px;
}

.landing-cta-secondary {
  display: inline-flex;
  align-items: center;
  padding: 13px 22px;
  font-size: 14px;
  font-weight: 500;
  color: var(--text-2);
  text-decoration: none;
  border-radius: 999px;
  transition: color 0.16s ease;
}

.landing-cta-secondary::after {
  content: "→";
  display: inline-block;
  margin-left: 8px;
  transition: transform 0.18s ease;
}

.landing-cta-secondary:hover {
  color: var(--text);
}

.landing-cta-secondary:hover::after {
  transform: translateX(3px);
}

.landing-cta-secondary:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 3px;
}

/* Scroll-hint line that breathes at the bottom of the hero. */
.landing-hero-scroll {
  position: absolute;
  bottom: 32px;
  left: 50%;
  transform: translateX(-50%);
  width: 1px;
  height: 44px;
  overflow: hidden;
  opacity: 0.5;
}

.landing-hero-scroll-line {
  display: block;
  width: 1px;
  height: 100%;
  background: linear-gradient(to bottom, transparent, var(--text-3), transparent);
  animation: landing-scroll-pulse 2.4s ease-in-out infinite;
}

@keyframes landing-scroll-pulse {
  0%, 100% { transform: translateY(-100%); }
  50%      { transform: translateY(100%); }
}

/* ── Features grid ─────────────────────────────────────────────────── */

.landing-features {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 24px;
}

.landing-feature {
  padding: 28px 24px;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: background 0.18s ease, border-color 0.18s ease, transform 0.18s ease;
}

.landing-feature:hover {
  background: var(--surface-2);
  border-color: var(--border-strong);
  transform: translateY(-2px);
}

.landing-feature-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 12px;
  background: rgba(95, 168, 255, 0.10);
  color: var(--blue);
  margin-bottom: 18px;
}

.landing-feature-title {
  font-size: 17px;
  font-weight: 600;
  color: var(--text);
  margin: 0 0 8px;
  letter-spacing: -0.01em;
}

.landing-feature-body {
  font-size: 14.5px;
  line-height: 1.55;
  color: var(--text-2);
  margin: 0;
}

/* ── Dashboard preview (CSS-rendered abstract mockup) ──────────────── */

.landing-preview-section {
  background:
    radial-gradient(ellipse at top, rgba(95, 168, 255, 0.07), transparent 60%),
    var(--bg);
}

/* CSS-only mockup of the real dashboard. Mirrors the two-panel layout
   (board on the left with score-tagged listing cards, detail panel on
   the right with AI BOOKING ASSESSMENT side-by-side score cards + action
   row), but every text element is a shape — no real titles, venue
   names, or scores leak. Update this block when the real dashboard
   layout shifts so the marketing page doesn't go stale. */

.landing-preview {
  position: relative;
  margin: 0 auto;
  max-width: 1020px;
  filter: blur(0.4px);
  opacity: 0.96;
}

.landing-preview-frame {
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  overflow: hidden;
  box-shadow:
    0 30px 80px rgba(0, 0, 0, 0.5),
    0 0 0 1px rgba(255, 255, 255, 0.02) inset;
}

.landing-preview-chrome {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 10px 14px;
  background: var(--sidebar);
  border-bottom: 1px solid var(--border);
}

.landing-preview-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.10);
}

.landing-preview-layout {
  display: grid;
  grid-template-columns: 120px 320px 1fr;
  min-height: 460px;
}

/* ── Sidebar mock ── */

.landing-preview-sidebar {
  background: var(--sidebar);
  padding: 16px 12px;
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.landing-preview-brand {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-bottom: 12px;
}

.landing-preview-brand-name {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text);
  line-height: 1;
}

.landing-preview-brand-sub {
  font-size: 8px;
  color: var(--text-3);
  line-height: 1;
}

.landing-preview-nav-item {
  height: 11px;
  background: rgba(255, 255, 255, 0.06);
  border-radius: 4px;
}

.landing-preview-nav-item.is-short {
  width: 60%;
}

.landing-preview-nav-item.is-active {
  height: auto;
  padding: 5px 6px;
  background: rgba(255, 255, 255, 0.16);
  border-left: 3px solid var(--accent);
  border-radius: 0 4px 4px 0;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: -0.005em;
  color: var(--text);
  white-space: nowrap;
}

/* ── Board (listing list) ── */

.landing-preview-board {
  background: #242424;
  padding: 14px 14px;
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.landing-preview-board-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 2px 8px;
  border-bottom: 1px solid var(--border);
}

.landing-preview-checkbox {
  width: 12px;
  height: 12px;
  border: 1px solid var(--border-strong);
  border-radius: 3px;
  flex: 0 0 auto;
}

.landing-preview-board-hint {
  flex: 1;
  height: 7px;
  background: rgba(255, 255, 255, 0.05);
  border-radius: 3px;
}

/* Listing card — mirrors the real .listing-card with two score tags
   on the left and a body block on the right (lineup → venue → footer). */

.landing-preview-card {
  display: flex;
  gap: 10px;
  padding: 10px 10px;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong);
  border-radius: 8px;
}

.landing-preview-card.is-selected {
  background: rgba(255, 255, 255, 0.05);
  border-color: rgba(255, 255, 255, 0.14);
  border-left: 3px solid var(--accent);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04);
}

.landing-preview-card-scores {
  display: flex;
  flex-direction: column;
  gap: 3px;
  flex: 0 0 auto;
}

.landing-preview-card-score {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  min-width: 58px;
  padding: 2px 6px;
  background: var(--amber-bg);
  border-radius: 4px;
}

.landing-preview-card-score-label {
  font-size: 7px;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--amber);
  text-transform: uppercase;
}

.landing-preview-card-score-value {
  font-size: 10px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--amber);
  letter-spacing: 0;
}

/* Score colour-banding so the mock board reads like the real one (high =
   green, mid = amber default, low = red) instead of a wall of amber. */
.landing-preview-card-score.is-green {
  background: var(--green-bg);
}
.landing-preview-card-score.is-green .landing-preview-card-score-label,
.landing-preview-card-score.is-green .landing-preview-card-score-value {
  color: var(--green);
}
.landing-preview-card-score.is-red {
  background: var(--red-bg);
}
.landing-preview-card-score.is-red .landing-preview-card-score-label,
.landing-preview-card-score.is-red .landing-preview-card-score-value {
  color: var(--red);
}

.landing-preview-card-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.landing-preview-card-line {
  height: 9px;
  width: 100%;
  background: rgba(255, 255, 255, 0.18);
  border-radius: 3px;
}

.landing-preview-card-line.is-short {
  width: 70%;
}

.landing-preview-card-sub {
  height: 7px;
  width: 60%;
  background: rgba(255, 255, 255, 0.07);
  border-radius: 3px;
}

.landing-preview-card-sub.is-short {
  width: 45%;
}

.landing-preview-card-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: 2px;
}

.landing-preview-card-date {
  flex: 1;
  height: 6px;
  background: rgba(255, 255, 255, 0.06);
  border-radius: 2px;
  max-width: 90px;
}

.landing-preview-pill {
  font-size: 8px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 2px 6px;
  border-radius: 999px;
  white-space: nowrap;
}

.landing-preview-pill.is-new {
  color: var(--blue);
  background: var(--blue-bg);
}

.landing-preview-pill.is-contacted {
  color: var(--amber);
  background: var(--amber-bg);
}

.landing-preview-pill.is-booked {
  color: var(--green);
  background: var(--green-bg);
}

/* ── Detail panel ── */

.landing-preview-detail {
  padding: 22px 24px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
}

.landing-preview-detail-title {
  height: 22px;
  width: 55%;
  background: rgba(255, 255, 255, 0.22);
  border-radius: 5px;
}

.landing-preview-detail-venue {
  height: 11px;
  width: 22%;
  background: rgba(255, 255, 255, 0.10);
  border-radius: 3px;
}

.landing-preview-detail-meta {
  display: flex;
  gap: 22px;
  align-items: flex-end;
  margin-top: 6px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--border);
}

.landing-preview-meta-cell {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.landing-preview-meta-label {
  font-size: 8px;
  font-weight: 700;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--text-3);
}

.landing-preview-meta-value {
  height: 8px;
  width: 56px;
  background: rgba(255, 255, 255, 0.14);
  border-radius: 3px;
}

.landing-preview-meta-value.is-pay {
  width: 44px;
  background: rgba(103, 214, 140, 0.32);
}

.landing-preview-meta-value.is-status {
  display: flex;
  align-items: center;
  gap: 5px;
  background: transparent;
  width: 70px;
  height: auto;
}

.landing-preview-meta-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--amber);
  flex: 0 0 auto;
}

.landing-preview-meta-value.is-status::after {
  content: "";
  flex: 1;
  height: 7px;
  background: rgba(255, 255, 255, 0.12);
  border-radius: 2px;
}

.landing-preview-section-heading {
  height: 9px;
  width: 32%;
  background: rgba(255, 255, 255, 0.12);
  border-radius: 3px;
  margin: 6px 0 2px;
}

/* AI BOOKING ASSESSMENT — two side-by-side score cards mirroring the
   real .ai-assessment .score-card-amber treatment. */

.landing-preview-ai-cards {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

.landing-preview-ai-card {
  padding: 14px 14px 12px;
  background: rgba(240, 169, 71, 0.06);
  border: 1px solid rgba(240, 169, 71, 0.18);
  border-left: 3px solid var(--amber);
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
}

/* Green band for a strong bookability score — mirrors the real
   .score-card-green treatment so the assessment doesn't read all-amber. */
.landing-preview-ai-card.is-green {
  background: rgba(103, 214, 140, 0.06);
  border-color: rgba(103, 214, 140, 0.20);
  border-left-color: var(--green);
}
.landing-preview-ai-card.is-green .landing-preview-ai-score {
  background: rgba(103, 214, 140, 0.16);
}
.landing-preview-ai-card.is-green .landing-preview-ai-score-value,
.landing-preview-ai-card.is-green .landing-preview-ai-score-denom {
  color: var(--green);
}

.landing-preview-ai-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.landing-preview-ai-label {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}

.landing-preview-ai-label-title {
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--text);
}

.landing-preview-ai-label-sub {
  display: block;
  height: 6px;
  width: 100px;
  background: rgba(255, 255, 255, 0.06);
  border-radius: 2px;
}

/* Tagline under each ASSESSMENT card title — mirrors the real
   ".ai-assessment-section-hint" copy. Small, muted, single line. */
.landing-preview-ai-label-hint {
  font-size: 9px;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--text-3);
  text-transform: none;
  line-height: 1.3;
}

.landing-preview-ai-score {
  display: inline-flex;
  align-items: baseline;
  gap: 1px;
  padding: 4px 10px;
  background: rgba(240, 169, 71, 0.14);
  border-radius: 6px;
}

.landing-preview-ai-score-value {
  font-size: 22px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--amber);
  letter-spacing: -0.02em;
  line-height: 1;
}

.landing-preview-ai-score-denom {
  font-size: 9px;
  font-weight: 500;
  color: var(--amber);
  opacity: 0.7;
  letter-spacing: 0;
}

.landing-preview-ai-body {
  height: 7px;
  width: 100%;
  background: rgba(255, 255, 255, 0.06);
  border-radius: 2px;
}

.landing-preview-ai-body.is-short {
  width: 75%;
}

/* Example rationale text inside each AI assessment card. Renders as
   real prose (not a placeholder bar) so visitors see what the AI
   actually outputs. Kept short + generic — no real venue/promoter
   names. Italic optional, but plain reads cleaner against the amber
   tint. */
.landing-preview-ai-text {
  margin: 0;
  font-size: 11px;
  line-height: 1.45;
  color: var(--text-2);
  letter-spacing: 0;
}

/* Action row — primary blue Draft outreach + outline Mark replied +
   ghost More on far right. */

.landing-preview-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 4px;
}

.landing-preview-btn {
  width: 90px;
  height: 22px;
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--border-strong);
}

.landing-preview-btn.is-primary {
  width: 100px;
  background: var(--blue);
  border-color: var(--blue);
  box-shadow: 0 0 18px rgba(95, 168, 255, 0.20);
}

.landing-preview-btn-more {
  width: 44px;
  height: 22px;
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border);
  margin-left: auto;
}

/* Soft fade overlay that blends the mockup into the section background.
   pointer-events:none so links underneath stay clickable if we ever
   layer real content into the frame. */
.landing-preview-fade {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    transparent 0%,
    transparent 60%,
    var(--bg) 100%
  );
  border-radius: 14px;
}

/* ── Waitlist form ─────────────────────────────────────────────────── */

.landing-waitlist-section {
  background: linear-gradient(to bottom, var(--bg) 0%, #181818 100%);
  padding-bottom: 120px;
}

.landing-waitlist-form {
  display: flex;
  flex-direction: column;
  gap: 18px;
  background: var(--surface-1);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: 32px;
  margin-top: 12px;
}

.landing-field {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.landing-field-label {
  font-size: 12px;
  font-weight: 500;
  color: var(--text-2);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
}

.landing-field-input {
  font: inherit;
  font-size: 14.5px;
  color: var(--text);
  background: rgba(255, 255, 255, 0.02);
  border: 1px solid var(--input-border);
  border-radius: var(--radius-sm);
  padding: 11px 14px;
  width: 100%;
  transition: border-color 0.16s ease, background 0.16s ease, box-shadow 0.16s ease;
}

.landing-field-input::placeholder {
  color: var(--text-3);
}

.landing-field-input:focus {
  outline: none;
  border-color: var(--blue);
  background: rgba(255, 255, 255, 0.04);
  box-shadow: 0 0 0 3px rgba(95, 168, 255, 0.18);
}

.landing-field-textarea {
  resize: vertical;
  min-height: 96px;
  line-height: 1.5;
}

.landing-form-actions {
  margin-top: 6px;
}

.landing-submit {
  width: 100%;
  position: relative;
  border: 0;
  font: inherit;
}

.landing-submit-spinner {
  display: none;
  width: 16px;
  height: 16px;
  border: 2px solid rgba(0, 0, 0, 0.25);
  border-top-color: #000;
  border-radius: 50%;
  margin-left: 8px;
  animation: backstage-spin 0.8s linear infinite;
}

.landing-submit.is-loading {
  pointer-events: none;
  opacity: 0.85;
}

.landing-submit.is-loading .landing-submit-spinner {
  display: inline-block;
}

.landing-form-status {
  min-height: 20px;
  font-size: 14px;
  color: var(--text-2);
  text-align: center;
  transition: color 0.2s ease;
}

.landing-form-status.is-success {
  color: var(--green);
  font-weight: 500;
}

.landing-form-status.is-error {
  color: var(--red);
}

/* Small pricing + privacy reassurance directly under the submit button —
   answers "what does it cost / will you spam me" right at the point of
   the ask. Muted + centered so it supports the button without competing. */
.landing-form-note {
  margin: 12px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--text-3);
  text-align: center;
}

/* Success-state replacement when the form submits via fetch. The
   form itself is hidden and this card slides in. */
.landing-waitlist-success {
  display: none;
  background: var(--green-bg);
  border: 1px solid rgba(103, 214, 140, 0.35);
  border-radius: var(--radius);
  padding: 32px;
  margin-top: 12px;
  text-align: center;
  animation: backstage-fade-in 0.4s ease-out;
}

.landing-waitlist-success.is-active {
  display: block;
}

.landing-waitlist-success-title {
  font-size: 18px;
  font-weight: 600;
  color: var(--green);
  margin: 0 0 8px;
}

.landing-waitlist-success-body {
  font-size: 14.5px;
  color: var(--text-2);
  margin: 0;
}

/* ── Footer ────────────────────────────────────────────────────────── */

.landing-footer {
  padding: 48px 32px;
  border-top: 1px solid var(--border);
  background: #161616;
}

.landing-footer-inner {
  max-width: 1080px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 16px;
}

.landing-footer-brand {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
}

.landing-footer-links {
  display: inline-flex;
  align-items: center;
  gap: 20px;
}

.landing-footer-link {
  font-size: 13px;
  color: var(--text-3);
  text-decoration: none;
  transition: color 0.14s ease;
}

.landing-footer-link:hover {
  color: var(--text);
}

.landing-footer-link:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
  border-radius: 2px;
}

.landing-footer-copy {
  font-size: 12px;
  color: var(--text-3);
}

/* ── Scroll-entrance animation ───────────────────────────────────────
   `data-anim` elements default to hidden so app.js's IntersectionObserver
   can swap them in via `.is-visible` for the fade-in-on-scroll effect.
   When JS is unavailable (CSP miss, asset 404, NoScript), the landing
   template carries `<noscript><link rel="stylesheet" href="/no-anim.css"></noscript>`
   which sets opacity back to 1 — so a JS-disabled user sees the whole
   page in its rested state instead of a blank wall below the hero.
   The fallback CSS file is `dashboard/public/no-anim.css`. */

[data-anim] {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.7s cubic-bezier(0.2, 0.7, 0.2, 1),
              transform 0.7s cubic-bezier(0.2, 0.7, 0.2, 1);
  will-change: opacity, transform;
}

[data-anim].is-visible {
  opacity: 1;
  transform: none;
}

/* Mobile + reduced-motion overrides */

@media (max-width: 720px) {
  .landing-header {
    padding: 12px 18px;
  }
  /* Two header pills (Get early access + Log in) plus the brand have to share
     a phone-width row — tighten the gap + pill padding so they don't overflow
     and clip the login button off the right edge. */
  .landing-header-nav {
    gap: 8px;
  }
  .landing-header-link {
    padding: 6px 11px;
  }
  .landing-section {
    padding: 64px 20px;
  }
  .landing-hero {
    padding: 100px 20px 60px;
  }
  /* The hero column is a flex item; without min-width:0 it refuses to
     shrink below its content's intrinsic width, so long lines push past
     the viewport and get clipped by overflow-x:hidden. Let it shrink. */
  .landing-hero-inner {
    min-width: 0;
    max-width: 100%;
  }
  /* On phones the hard line breaks force "Find better live-event" onto one
     line that's wider than the viewport. Drop the breaks so the headline
     wraps naturally + balanced, and size it down to fit a ~360px screen. */
  .landing-hero-br {
    display: none;
  }
  .landing-hero-title {
    font-size: 34px;
  }
  /* The eyebrow pill is one long uppercase line; shrink it and let it wrap
     so it can't overflow the narrow viewport. */
  .landing-eyebrow {
    font-size: 10.5px;
    letter-spacing: 0.10em;
    white-space: normal;
    max-width: 100%;
  }
  .landing-section-title {
    font-size: 30px;
  }
  .landing-section-sub {
    font-size: 15.5px;
    margin-bottom: 32px;
  }
  .landing-hero-sub {
    font-size: 16px;
  }
  .landing-features {
    grid-template-columns: 1fr;
    gap: 14px;
  }
  .landing-feature {
    padding: 22px 20px;
  }
  .landing-preview-layout {
    grid-template-columns: 90px 1fr;
  }
  .landing-preview-detail {
    display: none;
  }
  .landing-waitlist-form {
    padding: 22px;
  }
  .landing-footer-inner {
    flex-direction: column;
    align-items: flex-start;
  }
}

/* ── Landing: explainer ────────────────────────────────────────────── */

.landing-explainer-section {
  padding-top: 64px;
  padding-bottom: 64px;
  text-align: center;
}

.landing-explainer-tagline {
  font-size: 13px;
  font-style: italic;
  color: var(--text-3);
  letter-spacing: 0.01em;
  margin: 0 0 18px;
}

.landing-explainer-title {
  font-size: 28px;
  line-height: 1.2;
  font-weight: 600;
  color: var(--text);
  letter-spacing: -0.015em;
  margin: 0 0 18px;
  text-wrap: balance;
}

.landing-explainer-body {
  font-size: 16px;
  line-height: 1.6;
  color: var(--text-2);
  margin: 0 auto;
  max-width: 620px;
  text-wrap: pretty;
}

/* ── Landing: FAQ ──────────────────────────────────────────────────── */

.landing-faq-section {
  padding-top: 80px;
  padding-bottom: 80px;
}

.landing-faq {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.landing-faq-item {
  padding: 22px 24px;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.landing-faq-q {
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
  margin: 0 0 8px;
  letter-spacing: -0.005em;
}

.landing-faq-a {
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--text-2);
  margin: 0;
}

/* ── Landing: founder note ─────────────────────────────────────────────
   First-person trust signal between FAQ and waitlist. Deliberately NOT a
   bordered card — open, centred, airy so it reads as a personal voice
   rather than another product tile. */

.landing-founder-section {
  padding-top: 72px;
  padding-bottom: 72px;
  text-align: center;
}

.landing-founder-eyebrow {
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-3);
  margin: 0 0 16px;
}

.landing-founder-title {
  font-size: 26px;
  line-height: 1.2;
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--text);
  margin: 0 0 22px;
  text-wrap: balance;
}

/* Left-aligned inside the centred narrow column — a four-sentence
   paragraph reads better flush-left than centred. */
.landing-founder-body {
  font-size: 16px;
  line-height: 1.7;
  color: var(--text-2);
  margin: 0 auto 24px;
  max-width: 580px;
  text-align: left;
  text-wrap: pretty;
}

.landing-founder-sign {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  margin: 0;
  letter-spacing: -0.005em;
}

.landing-founder-sign span {
  color: var(--text-3);
  font-weight: 400;
}

/* ── Landing: entity summary ───────────────────────────────────────── */

.landing-summary-section {
  padding: 56px 32px 80px;
}

.landing-summary-title {
  font-size: 13px;
  font-weight: 500;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--text-3);
  margin: 0 0 20px;
}

.landing-summary-list {
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
  border-top: 1px solid var(--border);
}

.landing-summary-row {
  display: grid;
  grid-template-columns: 140px 1fr;
  gap: 16px;
  padding: 12px 0;
  border-bottom: 1px solid var(--border);
  align-items: baseline;
}

.landing-summary-row dt {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--text-3);
  margin: 0;
}

.landing-summary-row dd {
  font-size: 14.5px;
  line-height: 1.5;
  color: var(--text-2);
  margin: 0;
}

@media (max-width: 640px) {
  .landing-summary-row {
    grid-template-columns: 1fr;
    gap: 4px;
  }
  /* On the narrowest phones, drop the "by Invision" subtext so the brand +
     both header pills (Get early access + Log in) fit one row without the
     login button clipping off the right edge. */
  .landing-brand-sub {
    display: none;
  }
  .landing-header-link {
    font-size: 12px;
    padding: 6px 10px;
  }
}

/* ── Landing: optional-field hint ──────────────────────────────────── */

.landing-field-optional {
  margin-left: 6px;
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  color: var(--text-3);
}

@media (prefers-reduced-motion: reduce) {
  .landing-hero-orb,
  .landing-hero-scroll-line,
  .landing-submit-spinner {
    animation: none;
  }
  [data-anim] {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ── Score feedback (👍 / 👎 in the AI assessment card) ─────────────
   Sits at the bottom of the bookability .score-card. Reads as part of
   the score's own subgroup, not the worth_it card. Tight padding so
   it doesn't dominate the rationale paragraph above it. */
.score-feedback {
  margin-top: 4px;
  padding-top: 10px;
  border-top: 1px dashed var(--border);
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.score-feedback-prompt {
  margin: 0;
  font-size: 12px;
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  font-weight: 600;
}

.score-feedback-form {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.score-feedback-buttons {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.score-feedback-btn {
  background: transparent;
  border: 1px solid var(--border-strong);
  color: var(--text-2);
  padding: 5px 10px;
  border-radius: var(--radius-sm);
  font-size: 12px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
  font-family: inherit;
}

.score-feedback-btn:hover {
  border-color: var(--accent);
  color: var(--text);
}

.score-feedback-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.score-feedback-btn.is-active.score-feedback-up {
  border-color: var(--green);
  color: var(--green);
  background: var(--green-bg);
}

.score-feedback-btn.is-active.score-feedback-down {
  border-color: var(--red);
  color: var(--red);
  background: var(--red-bg);
}

.score-feedback-btn-label {
  font-weight: 500;
}

.score-feedback-clear {
  font-size: 11px;
  padding: 4px 8px;
  color: var(--text-3);
}

.score-feedback-count {
  margin-left: auto;
  font-size: 11px;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}

.score-feedback-note-wrap {
  border: none;
  padding: 0;
}

.score-feedback-note-wrap > summary {
  font-size: 12px;
  color: var(--text-3);
  cursor: pointer;
  padding: 2px 0;
  list-style: revert;
}

.score-feedback-note-wrap > summary:hover {
  color: var(--text-2);
}

.score-feedback-note-label {
  display: block;
  font-size: 11px;
  color: var(--text-3);
  margin: 6px 0 4px;
}

.score-feedback-note {
  width: 100%;
  font-family: inherit;
  font-size: 13px;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 6px 8px;
  resize: vertical;
  min-height: 44px;
  box-sizing: border-box;
}

.score-feedback-note:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.score-feedback-status {
  margin: 0;
  font-size: 11px;
  color: var(--text-3);
  min-height: 1em;
}

.score-feedback-status.is-success {
  color: var(--green);
}

.score-feedback-status.is-error {
  color: var(--red);
}

.score-feedback-form.is-loading .score-feedback-btn {
  opacity: 0.6;
  pointer-events: none;
}

.score-feedback-locked {
  margin: 6px 0 0;
  font-size: 11px;
  color: var(--text-3);
}
.score-feedback-locked a {
  color: var(--text-2);
}

/* ── Calendar (month grid view) ──────────────────────────────────────────
   Renders inside .main like .board: a flex column that owns its own scroll.
   Reuses the board's tokens, status dot colours, .panel surface, and the
   <details> disclosure idiom. Motion is handled by the global
   prefers-reduced-motion reset near the bottom of this file. */

.cal-topbar {
  flex-wrap: wrap;
}

.cal-monthnav {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.cal-nav-arrow {
  min-width: 30px;
  text-align: center;
  font-size: 15px;
  line-height: 1;
  padding: 5px 10px;
}

.cal-month-label {
  min-width: 116px;
  text-align: center;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}

.cal-today-link {
  margin-left: 4px;
}

.cal-legend {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 14px;
  margin: 0;
  padding: 0 0 0 4px;
  list-style: none;
}

.cal-legend .status-pill {
  cursor: default;
}

.cal-export-icon {
  font-size: 13px;
  line-height: 1;
}

/* Month / Week view switcher — a segmented control reading as a sibling of
   the board's Cards/List toggle (same border + active blue-tint treatment). */
.cal-viewswitch {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.18);
  overflow: hidden;
}
.cal-view-btn {
  border: none;
  background: transparent;
  color: var(--text-3);
  font-size: 12.5px;
  font-weight: 500;
  padding: 6px 13px;
  cursor: pointer;
  white-space: nowrap;
  text-decoration: none;
}
.cal-view-btn + .cal-view-btn {
  border-left: 1px solid var(--border);
}
.cal-view-btn:hover {
  color: var(--text);
}
.cal-view-btn.active {
  color: #fff;
  background: rgba(120, 170, 255, 0.12);
  font-weight: 600;
}
.cal-view-btn:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: -2px;
}

/* Jump-to-month: the big date label is a <details> summary that opens a
   month picker popover (zero-JS; app.js auto-submits on change as polish). */
.cal-jump {
  position: relative;
}
.cal-jump .cal-month-label {
  cursor: pointer;
  list-style: none;
  padding: 5px 8px;
  border-radius: var(--radius-sm);
  user-select: none;
}
.cal-jump .cal-month-label::-webkit-details-marker {
  display: none;
}
.cal-jump .cal-month-label:hover {
  background: rgba(255, 255, 255, 0.05);
}
.cal-jump[open] .cal-month-label {
  background: rgba(255, 255, 255, 0.07);
}
.cal-jump-form {
  position: absolute;
  top: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 30;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
  white-space: nowrap;
}
.cal-jump-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-3);
}
.cal-jump-row {
  display: flex;
  gap: 8px;
  align-items: center;
}
.cal-jump-input {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: 13px;
}
.cal-jump-input::-webkit-calendar-picker-indicator {
  filter: invert(0.7);
  cursor: pointer;
}
.cal-jump-go {
  white-space: nowrap;
}

.cal-board {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
  /* No top padding: the panel sits flush at the top of this scroll area so
     the sticky weekday/day header has nothing to bleed through above it. */
  padding: 0 16px 16px;
  overflow-y: auto;
}

.cal-panel {
  display: flex;
  flex-direction: column;
  flex: 0 0 auto;            /* natural height — cal-board owns the scroll */
  padding: 0;
  /* clip (not hidden/auto) so it isn't a scroll container — that lets the
     sticky header inside it resolve to .cal-board, while corners stay clipped. */
  overflow: clip;
}

.cal-weekdays,
.cal-grid {
  display: grid;
  /* minmax(0, 1fr) — NOT plain 1fr — so a long, unbreakable event title can't
     blow the track's min-content out past its 1/7 share and warp the columns.
     The pill's .cal-event-title ellipsis then truncates to the column width. */
  grid-template-columns: repeat(7, minmax(0, 1fr));
}

.cal-weekdays {
  flex: 0 0 auto;
  position: sticky;
  top: 0;
  z-index: 3;
  border-bottom: 1px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
  /* opaque over the panel so scrolled pills don't bleed through the header */
  background: linear-gradient(rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.02)), var(--panel);
}

.cal-weekday {
  padding: 8px 10px;
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-3);
  text-align: left;
}

.cal-grid {
  flex: 1 1 auto;
  min-height: 0;
}

.cal-day {
  position: relative;
  min-height: 108px;
  min-width: 0;              /* let the track shrink — pairs with minmax(0,1fr) */
  padding: 6px 6px 8px;
  border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 4px;
  background: var(--panel);
  transition: background 0.14s ease;
}

.cal-day:nth-child(7n) {
  border-right: 0;
}

.cal-day:hover {
  background: rgba(255, 255, 255, 0.025);
}

.cal-day-out {
  background: rgba(0, 0, 0, 0.16);
}

.cal-day-out .cal-day-num {
  color: var(--text-3);
}

.cal-day-today {
  box-shadow: inset 0 0 0 1px rgba(95, 168, 255, 0.45);
}

.cal-day-today .cal-day-num {
  background: var(--blue);
  color: #000;
  font-weight: 700;
}

.cal-day-num {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text-2);
  text-decoration: none;
  transition: background 0.12s ease, color 0.12s ease;
}

.cal-day-num:hover {
  background: rgba(255, 255, 255, 0.08);
  color: var(--text);
}

.cal-day-num:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 1px;
}

.cal-events {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}

.cal-event {
  display: flex;
  align-items: center;
  gap: 5px;
  min-width: 0;
  padding: 2px 6px 2px 4px;
  border-radius: 5px;
  background: var(--surface-1);
  color: var(--text);
  text-decoration: none;
  font-size: 11.5px;
  line-height: 1.3;
  transition: background 0.12s ease;
}

.cal-event:hover {
  background: var(--surface-2);
}

.cal-event:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 1px;
}

.cal-event-bar {
  flex: 0 0 auto;
  width: 3px;
  align-self: stretch;
  min-height: 13px;
  border-radius: 2px;
  background: var(--text-3);
}

.cal-event.status-new          .cal-event-bar { background: var(--blue); }
.cal-event.status-shortlisted  .cal-event-bar { background: var(--amber); }
.cal-event.status-contacted    .cal-event-bar { background: var(--yellow); }
.cal-event.status-booked       .cal-event-bar { background: var(--green); }
.cal-event.status-dismissed    .cal-event-bar { background: var(--red); }

.cal-event-time {
  flex: 0 0 auto;
  font-size: 10.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text-3);
}

.cal-event-title {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cal-event-past {
  opacity: 0.5;
}

.cal-event-past:hover {
  opacity: 0.8;
}

/* Multi-day festival spans: one pill per day, inner corners flattened +
   a › continuation cue so a week of cells reads as one run. */
.cal-event-span-start {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}
.cal-event-span-mid {
  border-radius: 0;
}
.cal-event-span-end {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}
.cal-event-span-mid .cal-event-title,
.cal-event-span-end .cal-event-title {
  visibility: hidden;
}
.cal-event-span-mid::after,
.cal-event-span-end::after {
  content: "›";
  color: var(--text-3);
  font-size: 11px;
}

/* Overflow "+N more" — native <details>, opens in place, no popover. */
.cal-more {
  margin-top: 1px;
}

.cal-more-toggle {
  cursor: pointer;
  list-style: none;
  font-size: 11px;
  font-weight: 600;
  color: var(--text-3);
  padding: 1px 4px;
  border-radius: 4px;
  user-select: none;
}
.cal-more-toggle::-webkit-details-marker { display: none; }

.cal-more-toggle:hover {
  color: var(--text-2);
  background: rgba(255, 255, 255, 0.04);
}

.cal-more-toggle:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 1px;
}

.cal-more[open] .cal-more-toggle {
  color: var(--text-2);
}

.cal-events-overflow {
  margin-top: 3px;
}

/* ── Month view: fit the whole grid + undated tray in the viewport ─────────
   On desktop the board doesn't scroll: the 6 week-rows divide the available
   height (grid-auto-rows: minmax(0,1fr)) and each cell clips overflow, so the
   grid + undated tray always fit. The "+N more" disclosure opens as a popover
   that escapes the clip. Mobile (≤880px, further down) keeps the stacked
   agenda + normal page scroll. */
@media (min-width: 881px) {
  .cal-board-month {
    overflow: hidden;        /* no internal scroll — content compresses to fit */
  }
  .cal-board-month .cal-panel {
    flex: 1 1 auto;
    min-height: 0;
    overflow: visible;       /* let "+N more" popovers escape the panel edge */
  }
  .cal-board-month .cal-grid {
    grid-auto-rows: minmax(0, 1fr);   /* equal rows fill the panel height */
    min-height: 0;
  }
  .cal-board-month .cal-day {
    min-height: 0;           /* rows shrink to share the height */
    overflow: hidden;        /* clip pills that don't fit the shortened cell */
  }

  /* "+N more" → popover anchored to the toggle; lift the active cell's clip
     so it can paint over its neighbours (Google-Calendar-style). */
  .cal-board-month .cal-day:has(.cal-more[open]) {
    overflow: visible;
    z-index: 6;
  }
  .cal-board-month .cal-more {
    position: relative;
  }
  .cal-board-month .cal-more[open] .cal-events-overflow {
    position: absolute;
    top: calc(100% + 3px);
    left: -4px;
    min-width: 156px;
    max-width: 240px;
    margin-top: 0;
    padding: 6px;
    z-index: 20;
    background: var(--panel);
    border: 1px solid var(--border-strong);
    border-radius: 8px;
    box-shadow: 0 12px 30px rgba(0, 0, 0, 0.5);
  }

  /* Undated tray: capped + internally scrollable so it never pushes the grid
     into a page scroll. */
  .cal-board-month .cal-undated-list {
    max-height: 104px;
    overflow-y: auto;
  }
}

/* Undated tray — persistent collapsible panel below the grid. */
.cal-undated {
  flex: 0 0 auto;
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: 4px 6px;
}

.cal-undated-summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  user-select: none;
}
.cal-undated-summary::-webkit-details-marker { display: none; }

.cal-undated-summary:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: -2px;
  border-radius: var(--radius-sm);
}

.cal-undated-heading {
  margin: 0;
}

.cal-undated-count {
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}

.cal-undated-hint {
  font-size: 11.5px;
  color: var(--text-3);
  font-style: italic;
}

.cal-undated-caret {
  margin-left: auto;
  font-size: 10px;
  color: var(--text-3);
  transition: transform 0.14s ease;
}

.cal-undated[open] .cal-undated-caret {
  transform: rotate(180deg);
}

.cal-undated-list {
  list-style: none;
  margin: 0;
  padding: 4px 8px 10px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 4px;
}

.cal-event-undated {
  background: var(--surface-1);
}

.cal-undated-flag {
  flex: 0 0 auto;
  margin-left: auto;
  font-size: 9.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-3);
}

/* ── Week view ─────────────────────────────────────────────────────────
   Seven Sun–Sat day-columns (equal height via the grid). Each column owns
   its own sticky header so the row of day labels stays put while scrolling
   AND the whole thing collapses to a stacked agenda on mobile. */
.cal-week-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
}
.cal-week-day {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 0 6px 10px;
  min-height: 240px;
  min-width: 0;
  border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.cal-week-day:nth-child(7n) {
  border-right: 0;
}
.cal-week-day-today {
  background: rgba(120, 170, 255, 0.06);
}
.cal-week-dayhead {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin: 0 -6px 4px;        /* full-bleed across the column's side padding */
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  background: var(--panel);   /* opaque so events scroll cleanly under it */
  text-decoration: none;
  color: var(--text-2);
}
.cal-week-dayhead:hover {
  color: var(--text);
}
.cal-week-dayhead:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: -2px;
}
.cal-week-dow {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-3);
}
.cal-week-daynum {
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.cal-week-day-today .cal-week-daynum {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  height: 24px;
  padding: 0 6px;
  background: var(--blue);
  color: #fff;
  border-radius: 12px;
}
.cal-week-empty {
  color: var(--text-3);
  font-size: 13px;
  padding: 2px 4px;
}
/* Columns are tall, so let week pills wrap to two lines + show the time
   above a wrapped title rather than truncating to one line. */
.cal-week-events .cal-event {
  align-items: flex-start;
}
.cal-week-events .cal-event-title {
  white-space: normal;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ── Calendar hover preview ────────────────────────────────────────────────
   A floating card app.js builds once and positions (position:fixed) next to
   the hovered/focused event pill — so it escapes the cell's overflow clip.
   Reads its content from the pill's data-cal-* attributes. */
.cal-preview {
  position: fixed;
  z-index: 1000;
  width: 264px;
  max-width: calc(100vw - 24px);
  padding: 12px 13px;
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  box-shadow: 0 16px 42px rgba(0, 0, 0, 0.55);
  color: var(--text);
  font-size: 12.5px;
  line-height: 1.4;
}
.cal-preview[hidden] {
  display: none;
}
.cal-preview-title {
  font-size: 13.5px;
  font-weight: 600;
  line-height: 1.3;
  color: #fff;
}
.cal-preview-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
}
.cal-preview-row .status-pill {
  font-size: 10px;
}
.cal-preview-when {
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.cal-preview-scores {
  display: inline-flex;
  gap: 6px;
}
.cal-preview-score {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  padding: 2px 8px;
  border-radius: 5px;
  font-weight: 700;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.cal-preview-score-label {
  font-size: 9px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  opacity: 0.8;
}
.cal-preview-pay {
  font-weight: 700;
  color: var(--green);
  font-variant-numeric: tabular-nums;
}
.cal-preview-source,
.cal-preview-location {
  color: var(--text-3);
  font-size: 11.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cal-preview-links {
  display: flex;
  gap: 14px;
  margin-top: 11px;
  padding-top: 9px;
  border-top: 1px solid var(--border);
}
.cal-preview-open,
.cal-preview-link {
  color: var(--blue);
  text-decoration: none;
  font-weight: 600;
  font-size: 12px;
}
.cal-preview-open:hover,
.cal-preview-link:hover {
  text-decoration: underline;
}

/* Detail-panel "Add to calendar" inline link (next to the Date meta value). */
.cal-add-link {
  margin-left: 10px;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--blue);
  text-decoration: none;
  white-space: nowrap;
}
.cal-add-link:hover {
  text-decoration: underline;
}

/* Settings: calendar-feed URL field + actions. */
.calendar-feed-url {
  width: 100%;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
}
.calendar-feed-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
}

/* Mobile: grid → vertical agenda at the board's 880px stack breakpoint.
   A 7-col grid is unusable on a phone; show only days-with-events as rows. */
@media (max-width: 880px) {
  .cal-board {
    overflow-y: visible;
  }

  .cal-weekdays {
    display: none;
  }

  .cal-grid {
    display: flex;
    flex-direction: column;
  }

  .cal-day-empty,
  .cal-day-out {
    display: none;
  }

  .cal-day {
    min-height: 0;
    flex-direction: row;
    align-items: flex-start;
    gap: 12px;
    border-right: 0;
    padding: 10px 12px;
  }

  .cal-day-num {
    flex: 0 0 auto;
    align-self: flex-start;
  }

  .cal-events {
    flex: 1 1 auto;
    gap: 5px;
  }

  .cal-event {
    font-size: 13px;
    padding: 6px 10px 6px 6px;
  }

  .cal-event-span-start,
  .cal-event-span-mid,
  .cal-event-span-end {
    border-radius: 5px;
  }
  .cal-event-span-mid .cal-event-title,
  .cal-event-span-end .cal-event-title {
    visibility: visible;
  }
  .cal-event-span-mid::after,
  .cal-event-span-end::after {
    content: none;
  }

  .cal-undated-list {
    grid-template-columns: 1fr;
  }

  /* Week view → stacked agenda: one day per row, only days with events,
     per-column headers go static (the page scrolls on mobile, so a
     viewport-sticky header would float over content). */
  .cal-week-grid {
    grid-template-columns: 1fr;
  }
  .cal-week-day {
    min-height: 0;
    border-right: 0;
  }
  .cal-week-day-empty {
    display: none;
  }
  .cal-week-dayhead {
    position: static;
    margin: 0 0 4px;
  }

  /* Keep the jump-to-month popover inside a narrow viewport. */
  .cal-jump-form {
    max-width: calc(100vw - 24px);
  }
}

/* ── Admin Console: focused-page filter toolbars + empty states ────────────
   Added with the /admin → Overview / Users / Scrapers / Logs split
   (2026-06-21). Server-side GET filters, so these are plain form controls
   styled to match the existing .admin-source-field inputs. The page nav
   reuses .admin-subnav / .admin-subnav-link (active set server-side via
   .is-active in admin_nav.erb). */

/* Status pills new to the unified users table. .admin-pill base already sets
   shape/size; these just add the colour pairing (green=allowlisted,
   amber=pending), matching the score-band palette used elsewhere. */
.admin-pill-allowed { background: var(--green-bg); color: var(--green); }
.admin-pill-pending { background: var(--amber-bg); color: var(--amber); }

/* Clickable Overview stat cards (deep-link to the focused pages). Inherits the
   .stats-card box; just removes link chrome + adds a hover affordance. */
.stats-card-link {
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  transition: border-color 0.14s ease, background 0.14s ease, transform 0.1s ease;
}
.stats-card-link:hover {
  border-color: var(--text-3);
  background: var(--surface-1);
  transform: translateY(-1px);
}
.stats-card-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Filter toolbar — one row of labelled controls + an actions line carrying
   Apply / Clear / the "Showing X of Y" count. Sits at the top of a card body,
   above the table it filters. */
.admin-filterbar {
  margin: 0 0 16px;
  padding: 14px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.admin-filter-fields {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 14px;
  align-items: flex-end;
}
.admin-filter-field {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}
.admin-filter-search {
  flex: 1 1 220px;
}
.admin-filter-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-3);
}
.admin-filter-hint {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
}
.admin-filterbar input[type="search"],
.admin-filterbar input[type="date"],
.admin-filterbar select {
  padding: 8px 11px;
  background: rgba(0, 0, 0, 0.28);
  border: 1px solid var(--input-border);
  border-radius: 6px;
  color: var(--text);
  font-family: inherit;
  font-size: 14px;
  transition: border-color 0.14s ease, background 0.14s ease;
}
.admin-filterbar input[type="search"] { width: 100%; }
.admin-filterbar input[type="search"]:hover,
.admin-filterbar input[type="date"]:hover,
.admin-filterbar select:hover {
  border-color: var(--text-3);
}
.admin-filterbar input[type="search"]:focus,
.admin-filterbar input[type="date"]:focus,
.admin-filterbar select:focus {
  outline: 2px solid rgba(120, 170, 255, 0.45);
  outline-offset: 1px;
  border-color: rgba(120, 170, 255, 0.6);
  background: rgba(0, 0, 0, 0.34);
}
/* "Errors only" — checkbox sits inline with its label (override the column). */
.admin-filter-check {
  flex-direction: row;
  align-items: center;
  gap: 7px;
  padding-bottom: 8px;
}
.admin-filter-check input[type="checkbox"] {
  width: 16px;
  height: 16px;
  accent-color: var(--blue);
}
.admin-filter-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin-top: 13px;
}
.admin-filter-count {
  margin: 0;
  margin-left: auto;
  font-size: 12.5px;
  color: var(--text-2);
}
/* Per-section count line (logs page) — sits above a table, not in the toolbar. */
.admin-filter-count-inline {
  margin: 0 0 10px;
  margin-left: 0;
}

/* Empty state — shown when a filter set matches nothing (distinct from the
   "no data at all yet" settings-hint). Centered, muted, with a Clear action. */
.admin-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 32px 20px;
  text-align: center;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-sm);
  background: rgba(0, 0, 0, 0.12);
}
.admin-empty-title {
  margin: 0;
  color: var(--text-2);
  font-size: 14px;
}

@media (max-width: 880px) {
  /* Stack the filter fields full-width so controls don't crowd on a phone. */
  .admin-filter-field { flex: 1 1 100%; }
  .admin-filter-count { margin-left: 0; }
}

/* Page-local refinement note (scraper-runs status/search can't go to SQL, so
   they filter the current page only) + the secondary hint line in an empty
   state. Shares the muted-info treatment of the filter toolbar. */
.admin-filter-note {
  margin: 0 0 14px;
  padding: 9px 12px;
  border: 1px solid var(--border);
  border-left: 3px solid var(--blue);
  border-radius: var(--radius-sm);
  background: rgba(120, 170, 255, 0.07);
  color: var(--text-2);
  font-size: 12.5px;
  line-height: 1.5;
}
.admin-empty-hint {
  margin: 0;
  color: var(--text-3);
  font-size: 12.5px;
}

/* ── Pagination (admin_pagination.erb) ────────────────────────────────────
   Reusable below every paginated Admin Console table (scraper runs, waitlist,
   deleted listings, score-feedback rollup, users). A summary line on the left,
   Prev / numbered-window / Next controls on the right. */
.admin-pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px 16px;
  margin-top: 14px;
}
.admin-pagination-summary {
  margin: 0;
  font-size: 12.5px;
  color: var(--text-2);
}
.admin-pagination-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
  margin-left: auto;
}
.admin-pagination-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 34px;
  padding: 6px 10px;
  border: 1px solid var(--input-border);
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.22);
  color: var(--text);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  text-decoration: none;
  transition: border-color 0.14s ease, background 0.14s ease, color 0.14s ease;
}
a.admin-pagination-link:hover {
  border-color: var(--text-3);
  background: rgba(0, 0, 0, 0.34);
}
a.admin-pagination-link:focus-visible {
  outline: 2px solid rgba(120, 170, 255, 0.45);
  outline-offset: 1px;
}
.admin-pagination-current {
  background: var(--blue);
  border-color: var(--blue);
  color: #06243f;
  font-weight: 600;
  cursor: default;
}
.admin-pagination-link.is-disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.admin-pagination-gap {
  padding: 0 4px;
  color: var(--text-3);
  user-select: none;
}

@media (max-width: 880px) {
  .admin-pagination { flex-direction: column; align-items: stretch; }
  .admin-pagination-controls { margin-left: 0; justify-content: center; }
}
