:root {
  --accent: #2e7d32;
  --accent-dark: #235e27;
  --accent-soft: #e7f3e8;
  --accent-contrast: #ffffff;
  --bg: #fafbfa;
  --surface: #ffffff;
  --text: #1a1f1b;
  --text-muted: #667066;
  --border: #e2e7e2;
  /* An approved Model credit's own gold/amber pair (.model-pill-approved,
     styles.css) — deliberately its own color, not --accent, so a starred
     Model chip reads as "reviewed identity" rather than blending into the
     same green/pink an ordinary tag pill already uses for both themes. */
  --model-gold: #7a5b00;
  --model-gold-soft: #fff3cd;
}

/* Dark palette — applies automatically when the visitor's OS/browser
   prefers dark (unless THEME=light pins it to light), and always when
   THEME=dark pins it explicitly. See lib/brand.js (getThemeAttr) and
   server.js, which set data-theme="light"/"dark" on <html> from the THEME
   env var, or leave it unset for "system" so this media query decides. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --accent: #eb004e;
    --accent-dark: #bc003e;
    --accent-soft: #3a0f1e;
    --accent-contrast: #ffffff;
    --bg: #1a1216;
    --surface: #221a1e;
    --text: #f4ecee;
    --text-muted: #a89aa0;
    --border: #3a2d32;
    --model-gold: #ffd666;
    --model-gold-soft: #3d2f0a;
  }
}

:root[data-theme="dark"] {
  --accent: #eb004e;
  --accent-dark: #bc003e;
  --accent-soft: #3a0f1e;
  --accent-contrast: #ffffff;
  --bg: #1a1216;
  --surface: #221a1e;
  --text: #f4ecee;
  --text-muted: #a89aa0;
  --border: #3a2d32;
  --model-gold: #ffd666;
  --model-gold-soft: #3d2f0a;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  color: var(--text);
  background: var(--bg);
  line-height: 1.6;
}

a {
  color: var(--accent);
}

.container {
  max-width: 860px;
  margin: 0 auto;
  padding: 0 20px;
}

/* ---- site header ---- */

header.site-header {
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}

header.site-header .container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: 16px;
  padding-bottom: 16px;
}

.brand {
  font-size: 17px;
  font-weight: 700;
  text-decoration: none;
  color: var(--text);
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Highlights the logo itself while on the Dashboard (round-32 punchlist,
   app.js's markActiveNavLink()) — same accent-color "currently on this page"
   treatment .site-nav a.active/.footer-links a.active already give every
   other active link, just applied to .brand instead since Dashboard has no
   nav link of its own to mark (the brand link already doubles as the
   Dashboard shortcut for a logged-in visitor — see hydrateSiteNav). */
.brand.active {
  color: var(--accent);
}

/* Home page only (index.html) — wraps .brand plus the optional BRAND_TAGLINE
   span so both stay one flex item within the header's own space-between
   layout (header.site-header .container), same as .brand alone was before.
   On every other page the tagline placeholder is never in the markup at
   all, so .brand-wrap there is just an unstyled, harmless wrapper. */
.brand-wrap {
  display: flex;
  align-items: baseline;
  gap: 10px;
  min-width: 0;
}

.brand-tagline {
  font-size: 13px;
  font-weight: 400;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (max-width: 640px) {
  .brand-tagline {
    display: none;
  }
}

/* Impersonate User (later round) — the sticky read-only-mode banner
   app.js's renderImpersonationBanner() prepends to <body> on every page
   while this browser holds an impersonation token (see that function's own
   comment for why this is the one deliberate exception to the feature's
   "invisible" design — invisible to the TARGET, not to the admin currently
   looking through their account). Danger-red, same tone this app already
   reserves for "something needs your attention" elsewhere (.btn-danger,
   .admin-legacy-user-status-delivery-bad) — impersonation is a normal,
   intended admin action, but the banner still needs to read as "not your
   own browsing session right now" at a glance, not blend in as ordinary
   page chrome. Sticky rather than fixed so it scrolls out of the way of
   nothing (no page content needs to shift down to make room below a fixed
   bar), but still reappears the instant you scroll back to the top of a
   long page.  z-index sits above .site-nav's own dropdowns (30) and every
   ordinary page element, but well below Quick Hide's deliberately-maximal
   one (2147483647, see that feature's own comment) since that's an
   unrelated full-page cover, not something this banner should compete
   with. */
.impersonation-banner {
  position: sticky;
  top: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 12px;
  padding: 10px 16px;
  background: #c62828;
  color: #fff;
  font-size: 14px;
  font-weight: 600;
  text-align: center;
}

.impersonation-banner .btn {
  padding: 6px 14px;
  font-size: 13px;
  background: #fff;
  border-color: #fff;
  color: #c62828;
}

.impersonation-banner .btn:hover {
  background: rgba(255, 255, 255, 0.85);
  border-color: rgba(255, 255, 255, 0.85);
  color: #a92121;
}

.site-nav {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  row-gap: 10px;
  column-gap: 14px;
  font-size: 14px;
}

.site-nav a {
  text-decoration: none;
  font-weight: 500;
}

/* :not(.btn) on both of these matters — a plain ".site-nav a" or
   ".site-nav a:hover" selector has higher specificity than ".btn-primary"/
   ".btn-primary:hover" (class+element beats class alone), so without this
   exclusion these would repaint a primary nav button's text back to the
   accent color — the same color as its own background (the "Create
   Account" button bug: accent-on-accent text, in both its default and
   hover states). Excluding .btn here lets button-classed nav links fall
   through entirely to the global .btn/.btn-primary rules below instead.
*/
.site-nav a:not(.btn) {
  color: var(--text-muted);
}

.site-nav a:not(.btn):hover {
  color: var(--accent);
}

/* The current page's own nav link — same accent color as the hover state
   above (so it reads as "you're already here" using the same visual
   language as "go here"), plus a heavier weight so it's still visibly
   distinct from an ordinary hover once the mouse moves away. Set by
   app.js's markActiveNavLink(), which runs on every page (part of
   initSharedNav). :not(.btn) for the same reason the hover rule above
   needs it — see that rule's own comment. */
.site-nav a.active:not(.btn) {
  color: var(--accent);
  font-weight: 600;
}

/* "Create Account" (/register) is a .btn-primary, not a plain nav link, so
   the :not(.btn) rule just above never touches it — its own accent-colored
   background already uses the same color the plain-link .active state
   above does, so recoloring text here would look identical to its ordinary
   state instead of visibly different. An inset ring in the button's own
   contrast color reads as "you're already here" without fighting its
   existing look. Set by the same app.js markActiveNavLink() as every other
   nav highlight here. */
.site-nav a.btn-primary.active {
  box-shadow: 0 0 0 2px var(--accent-contrast) inset;
}

.site-nav a.btn:not(.btn-primary) {
  color: var(--accent);
}

/* The Admin nav link — an icon + a live open-reports count badge (round-32
   nav-order request: "Admin (switch to admin-related icon)") in place of
   the old plain "Admin"/"Admin (n)" text link. Same plain-icon-button shape
   as .notif-bell just below (no border/background box, unlike
   .mask-mode-toggle/.topo-mode-toggle further down — this is a navigation
   LINK, not a toggle switch, so it keeps the nav's own plain-link look) —
   position: relative anchors .admin-nav-badge the same way .notif-bell-wrap
   anchors .notif-badge. */
.admin-nav-link {
  position: relative;
  display: inline-flex;
  align-items: center;
  padding: 4px;
}

.admin-nav-icon {
  font-size: 16px;
  line-height: 1;
}

/* Same shape/position as .notif-badge above (see that rule's own comment on
   why a badge instead of inline text now that this link is an icon, not a
   text label) — kept as its own class rather than sharing .notif-badge
   directly since the two badges anchor to differently-shaped parents
   (.admin-nav-link has no wrapping element of its own the way
   .notif-bell-wrap does) and may want to drift independently later. */
.admin-nav-badge {
  position: absolute;
  top: -4px;
  right: -6px;
  min-width: 15px;
  height: 15px;
  padding: 0 3px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-contrast);
  font-size: 10px;
  font-weight: 700;
  line-height: 15px;
  text-align: center;
}

/* .user-identity is an <a> even before a username is known (falls back to
   plain email text with its href removed — see dashboard.js renderIdentity)
   so it needs its own not-actually-a-link cursor, since :not(.btn) above
   already colors it like every other nav link regardless of href. */
.user-identity:not([href]) {
  cursor: default;
}

.user-identity:not([href]):hover {
  color: var(--text-muted);
}

/* Round 50/51: the pink Premium-membership checkmark next to the username
   in the main nav went through two prior fixes, both now superseded.
   Round 50 made it position:absolute inside #userIdentity to stop its
   late arrival (app.js's hydrateMemberNav(), an async follow-up to the
   rest of the nav's own load) from shoving every later flex item to the
   right. Round 51 then shrank it (10px/7px, down from 15px/10px) because
   absolute positioning had put it outside .site-nav's own 14px column-gap
   rhythm, and the full-size mark was actually wider than that gap, so no
   offset could avoid touching either the username or the bell.
   Reported a third time as still not right: even overlap-free, an
   absolutely-positioned element never actually participates in
   .site-nav's column-gap, so the spacing around it could never truly
   match the even rhythm between the nav's own text links no matter how
   its offset was tuned.
   Fixed properly this time in the markup itself, not CSS: the checkmark
   (#userMemberCheck, MEMBER_CHECK_NAV_HTML in lib/brand.js) is now
   rendered as #userIdentity's own NEXT SIBLING directly inside .site-nav,
   not nested inside #userIdentity at all — so it's a real flex item and
   .site-nav's existing column-gap:14px spaces it evenly on both sides for
   free, exactly like every other nav item, with no manual offset math and
   no shrinking. It's revealed/hidden by app.js's hydrateMemberNav()
   (toggling .hidden, not insertAdjacentHTML) and, on a return visit, by
   the cached-eligibility early-reveal script EARLY_MEMBER_NAV_SCRIPT_HTML
   (lib/brand.js) — the same pattern this file's own #adminNavLink and
   .mask-mode-toggle already use, so a returning eligible member sees it
   in place from the very first frame instead of a late pop-in. Only
   `margin-left: 0` is needed here, to cancel out .member-check's own
   3px margin-left below (meant for plain inline-text contexts) so it
   doesn't double up with .site-nav's own gap and throw off the even
   spacing on that one side. */
#userMemberCheck {
  margin-left: 0;
}

/* The notification bell (public/app.js's hydrateNotifications()) — lives in
   every page's .site-nav (see the shared header markup each static page
   and lib/publicPages.js's own siteHeader() all carry), same nav-logged-in
   show/hide convention as the rest of that nav except on the handful of
   always-logged-in pages (account.html/dashboard.html/membership.html)
   where it's unconditionally visible, matching #userIdentity's own
   treatment on those same pages. position: relative anchors
   .notif-dropdown below to this wrapper, not to the page. */
.notif-bell-wrap {
  position: relative;
  display: flex;
  align-items: center;
}

.notif-bell {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
  color: var(--text-muted);
  padding: 4px;
  position: relative;
}

.notif-bell:hover {
  color: var(--accent);
}

/* Unread count — a small solid dot with the number in it, same
   "impossible to miss, doesn't need to be read to register" convention the
   Admin nav link's own "(n)" open-reports count uses, just a badge instead
   of inline text since this sits on an icon, not a text label. Hidden
   outright (not just showing "0") whenever there's nothing unread — see
   app.js's hydrateNotifications(). */
.notif-badge {
  position: absolute;
  top: -4px;
  right: -6px;
  min-width: 15px;
  height: 15px;
  padding: 0 3px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-contrast);
  font-size: 10px;
  font-weight: 700;
  line-height: 15px;
  text-align: center;
}

/* Right-aligned under the bell (not centered/left) so it never runs off the
   right edge of the viewport regardless of where the bell itself sits in a
   wide .site-nav — the same reasoning a browser's own address-bar menus
   right-align from whichever icon opened them. */
.notif-dropdown {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  width: 320px;
  max-width: calc(100vw - 32px);
  max-height: 70vh;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
  z-index: 30;
  padding: 6px 0;
}

.notif-dropdown-header {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-muted);
  padding: 6px 14px 8px;
}

.notif-item {
  display: block;
  padding: 8px 14px;
  font-size: 13.5px;
  color: var(--text);
  text-decoration: none;
}

.notif-item:hover {
  background: var(--accent-soft);
}

/* An unread row's own left accent stripe — the badge count is already
   cleared the moment the dropdown opens (see app.js's own comment on why),
   so this is the one remaining "these are the ones you hadn't seen yet"
   signal once it has. */
.notif-item.unread {
  box-shadow: inset 3px 0 0 var(--accent);
}

.notif-item-time {
  display: block;
  font-size: 11.5px;
  color: var(--text-muted);
  margin-top: 2px;
}

.notif-empty {
  padding: 14px;
  font-size: 13px;
  color: var(--text-muted);
  text-align: center;
}

/* The user-icon nav dropdown (public/app.js's initUserMenu()) — Activity
   and Account, folded behind one icon the same way the bell above folds a
   whole notification list behind one icon. Same three-rule shape as
   .notif-bell-wrap/.notif-bell/.notif-dropdown just above (position:
   relative anchor, icon-only trigger button, absolutely-positioned
   right-aligned panel) — kept as its own small set of rules rather than
   reusing the .notif-* classes directly, since "this is the bell" and
   "this is the user menu" are different enough concepts that sharing a
   class name would just make each one's CSS harder to read in isolation,
   the same call every other pair of near-identical widgets in this
   stylesheet already makes (.view-toggle-btn vs .type-filter-btn, etc). */
.user-menu-wrap {
  position: relative;
  display: flex;
  align-items: center;
}

.user-menu-btn {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
  color: var(--text-muted);
  padding: 4px;
}

.user-menu-btn:hover {
  color: var(--accent);
}

/* Same "currently on this page" treatment .site-nav a.active gives a plain
   link — this is a <button>, not an <a>, so it needs its own rule rather
   than sharing that one (see app.js's markActiveNavLink() for when this
   gets added: visiting /activity or /account with the dropdown collapsed). */
.user-menu-btn.active {
  color: var(--accent);
}

.user-menu-dropdown {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  min-width: 160px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
  z-index: 30;
  padding: 6px 0;
  display: flex;
  flex-direction: column;
}

.user-menu-dropdown a {
  display: block;
  padding: 8px 14px;
  font-size: 13.5px;
  font-weight: 500;
  color: var(--text);
  text-decoration: none;
}

.user-menu-dropdown a:hover {
  background: var(--accent-soft);
}

/* Same reasoning as .site-nav a.active:not(.btn) above — highlights
   whichever of the two links inside this open dropdown matches the current
   page (app.js's markActiveNavLink()). No :not(.btn) needed here the way
   that rule needs it — neither link in this dropdown is ever styled
   .btn. */
.user-menu-dropdown a.active {
  color: var(--accent);
  font-weight: 600;
}

/* Hamburger toggle for .site-nav — hidden by default (desktop/tablet width,
   where .site-nav already lays out fine as an inline row) and only shown
   below the breakpoint in the media query further down, which is also where
   .site-nav itself switches from "always visible inline row" to "collapsed
   unless toggled open". app.js's initNavToggle() wires the click. */
.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  padding: 0;
}

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

@media (max-width: 640px) {
  .nav-toggle {
    display: inline-flex;
  }

  /* flex-wrap lets .site-nav (100% width below) drop to its own row under
     the brand+toggle row instead of needing separate absolute positioning —
     there's no page content beside the header for it to overlap either
     way. */
  header.site-header .container {
    flex-wrap: wrap;
  }

  .site-nav {
    display: none;
    width: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
    margin-top: 14px;
    padding-top: 14px;
    border-top: 1px solid var(--border);
  }

  /* Toggled by app.js's initNavToggle(), not a CSS-only checkbox hack —
     keeping it a plain class match app.js's other nav-state classes
     (nav-logged-out/nav-logged-in) use for the same "[hidden] is the only
     other thing already fighting .btn's `display` here" reason documented
     below. */
  .site-nav.nav-open {
    display: flex;
  }

  /* :not(.btn) so this doesn't fight .btn's own (larger, pill-shaped)
     padding — same reasoning as the a:not(.btn) color rule above this
     media query. */
  .site-nav a:not(.btn) {
    padding: 8px 4px;
  }

  .site-nav a.btn {
    width: 100%;
  }
}

/* The [hidden] attribute is how hydrateSiteNav() (app.js) toggles
   .nav-logged-out/.nav-logged-in — it relies on the browser's default
   `[hidden] { display: none }` UA rule. That default loses to ANY author
   rule that sets `display` on the same element regardless of specificity
   (author styles beat UA styles at equal importance), so `.btn`'s own
   `display: inline-flex` below was silently overriding it — a button-styled
   nav link like "Create Account" (.btn.btn-primary.nav-logged-out) stayed
   visible even while actually hidden="", because the .btn rule, not the
   browser default, was what actually painted it. This reinstates `hidden`
   as authoritative for every element, button-styled or not. */
[hidden] {
  display: none !important;
}

/* ---- buttons ---- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  padding: 10px 18px;
  cursor: pointer;
  text-decoration: none;
  font-family: inherit;
}

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

/* Round 32 follow-up #60 — the Legacy Users tab's "Send to all pending" and
   "Send reminder to non-responders" buttons go `disabled` in JS (public/
   admin.js's renderLegacyUsers) when there's nothing to send, but until now
   that was invisible: .btn sets its own background/color/border-color
   above, which beats the browser's native disabled dimming the same way
   .field input:disabled's own comment describes for form fields, and .btn's
   unconditional `cursor: pointer` above wasn't scoped to :not(:disabled)
   either — so a disabled send button looked, and felt (pointer cursor),
   exactly like a working one. Explicit here for the same "read correctly in
   both themes regardless of native rendering" reason .field input:disabled
   gives; :hover above is also disabled below so hovering a disabled button
   doesn't flash the active accent color right before the click does
   nothing. */
.btn:disabled {
  color: var(--text-muted);
  cursor: not-allowed;
}

.btn:disabled:hover {
  border-color: var(--border);
  color: var(--text-muted);
}

.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}

.btn-primary:hover {
  background: var(--accent-dark);
  border-color: var(--accent-dark);
  color: var(--accent-contrast);
}

.btn-block {
  width: 100%;
}

/* ---- hero / landing ---- */

.hero {
  padding: 72px 0 56px;
}

.hero h1 {
  font-size: 38px;
  margin: 0 0 14px;
  max-width: 16ch;
}

.hero p.lede {
  font-size: 17px;
  color: var(--text-muted);
  max-width: 52ch;
  margin: 0 0 28px;
}

.hero-actions {
  display: flex;
  gap: 12px;
  margin-bottom: 8px;
}

.how-it-works {
  padding: 8px 0 64px;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}

@media (max-width: 680px) {
  .how-it-works { grid-template-columns: 1fr; }
}

.how-step {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 20px;
}

.how-step .step-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 13px;
  font-weight: 700;
  margin-bottom: 12px;
}

.how-step h3 {
  font-size: 15px;
  margin: 0 0 6px;
}

.how-step p {
  font-size: 13.5px;
  color: var(--text-muted);
  margin: 0;
}

.footnote {
  color: var(--text-muted);
  font-size: 13.5px;
  padding-bottom: 60px;
}

/* ---- support page (/support) ---- */

.support-section {
  max-width: 680px;
  margin: 0 0 36px;
}

.support-section h2 {
  font-size: 18px;
  margin: 0 0 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
}

.support-section h3 {
  font-size: 14.5px;
  margin: 20px 0 6px;
}

.support-section p,
.support-section li {
  font-size: 14px;
  color: var(--text-muted);
}

.support-section ul {
  padding-left: 20px;
  margin: 0;
}

.support-section li {
  margin-bottom: 8px;
}

/* ---- site footer ---- */

/* Shared across every regular-chrome page (see lib/brand.js's
   getFooterHtml(), spliced in via server.js's {{SITE_FOOTER}} and
   lib/publicPages.js's layout()) — NOT shown on privacy-policy.html or
   terms.html, which are self-contained documents with their own
   single-purpose footer already, or on frame.html, a full-viewport iframe
   viewer with no room for one. */
.site-footer {
  margin-top: 48px;
  border-top: 1px solid var(--border);
  padding: 28px 0 36px;
}

.site-footer-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* flex-start, not space-between — space-between was spreading the row's
     two groups (nav links, copyright) to the far left and far right edges
     even with only 14px of actual content between them once the window was
     wide, which read as the copyright line floating off on its own rather
     than being part of the same footer. flex-start keeps everything
     clustered together, left-aligned, the same direction .footer-copyright
     itself already reads in. */
  justify-content: flex-start;
  gap: 14px;
}

/* Three stacked rows — social icon(s), then nav links, then copyright —
   each on its own line (flex: 1 0 100% forces a break both before and after
   a 100%-basis item), in that order both visually (order: 0/1/2) and in the
   markup itself (lib/brand.js's getFooterHtml). Used to be two rows (links
   + social sharing row 1, copyright alone on row 2) — split apart because a
   lone social icon sharing a row with the text links read as visually
   unbalanced/cluttered; three clean rows reads calmer. */
.footer-social {
  display: flex;
  gap: 12px;
  flex: 1 0 100%;
  order: 0;
}

.footer-social-link {
  display: inline-flex;
  align-items: center;
  color: var(--text-muted);
  text-decoration: none;
  font-size: 16px;
  line-height: 1;
}

.footer-social-link:hover {
  color: var(--accent);
}

/* Only Tumblr's own icon is an inline SVG (X's is a plain Unicode glyph,
   which needs none of this) — stroke=currentColor already makes it follow
   .footer-social-link's own color/hover color above, this just keeps its
   box the same 16px both link types otherwise share so the two sit on the
   same visual baseline in the row instead of the SVG's own default
   intrinsic sizing making it a slightly different size. */
.footer-social-link svg {
  width: 16px;
  height: 16px;
}

.footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  flex: 1 0 100%;
  order: 1;
}

.footer-links a {
  color: var(--text-muted);
  text-decoration: none;
  font-size: 13.5px;
}

.footer-links a:hover {
  color: var(--text);
  text-decoration: underline;
}

/* Round-32 punchlist — Partners/Support (app.js's markActiveNavLink()),
   same accent-color treatment .site-nav a.active already gives every other
   active link, just against the footer's own muted default instead of
   .site-nav's. */
.footer-links a.active {
  color: var(--accent);
  text-decoration: underline;
}

.footer-copyright {
  color: var(--text-muted);
  font-size: 12.5px;
  margin: 0;
  flex: 1 0 100%;
  order: 2;
}

/* ---- auth forms ---- */

.auth-wrap {
  max-width: 380px;
  margin: 64px auto;
  padding: 0 20px;
}

.auth-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 28px;
}

.auth-card h1 {
  font-size: 21px;
  margin: 0 0 6px;
}

.auth-card .auth-sub {
  color: var(--text-muted);
  font-size: 13.5px;
  margin: 0 0 22px;
}

/* ---- age gate ---- */
/* Reuses .auth-wrap/.auth-card's centered-card layout (public/age-gate.html)
   rather than a new one — same page shape as login/register, just with two
   stacked buttons instead of a form. */

.age-gate-brand {
  font-weight: 700;
  font-size: 16px;
  margin-bottom: 14px;
}

.age-gate-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.field {
  margin-bottom: 14px;
}

.field label {
  display: block;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-muted);
  margin-bottom: 5px;
}

.field input,
.field textarea {
  width: 100%;
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 14px;
  font-family: inherit;
}

.field input:focus,
.field textarea:focus {
  outline: none;
  border-color: var(--accent);
}

/* claim-account.html's own read-only "Email" field (the only disabled
   .field input anywhere on this site) — .field input above never sets its
   own background/color, relying on the browser's native input rendering
   instead, which is normally fine (a real white box, real black text) but
   goes muted-grey-on-muted-grey for a DISABLED field once either this
   site's own dark theme or the browser/OS's own forced-dark rendering gets
   involved, since neither of those touches a native disabled input's
   colors the same predictable way they touch this page's own CSS. Explicit
   theme colors here read correctly in both themes regardless. */
.field input:disabled {
  background: var(--bg);
  color: var(--text-muted);
  cursor: not-allowed;
}

/* Wraps a password <input> once app.js's wirePasswordToggle() runs — plain
   .field's own margin/label styling above is untouched, this just gives
   the input a positioning context for the icon button below. */
.password-field {
  position: relative;
}

/* Same box as any other .field input (the selector below matches that
   rule's own specificity, class+type+class vs. class+type, so this wins
   without !important) — just reserves room on the right so typed text
   never renders under the icon button. */
.field input.password-field-input {
  padding-right: 40px;
}

/* Deliberately NOT var(--text-muted)/var(--border) here, unlike most icon
   buttons elsewhere in this app — .field input above has no background/
   color rules of its own, so every text field on this site (this one
   included) renders with the browser's plain native white box regardless
   of site theme; --text-muted flips to a PALE color in dark mode (meant
   for reading against dark surfaces, not a white input box), which made
   this icon nearly invisible sitting on top of one. Fixed, theme-independent
   colors instead — chosen to read clearly on the light input box this
   button always sits on, in either theme. */
.password-toggle-btn {
  position: absolute;
  top: 50%;
  right: 4px;
  transform: translateY(-50%);
  width: 30px;
  height: 30px;
  padding: 0;
  border: none;
  border-radius: 6px;
  background: none;
  color: #667066;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.password-toggle-btn:hover {
  color: #1a1f1b;
  background: #e2e7e2;
}

.password-toggle-btn svg {
  width: 18px;
  height: 18px;
}

.field-checkbox {
  margin-bottom: 16px;
}

/* Scoped as ".field-checkbox .checkbox-label" rather than bare
   ".checkbox-label" — this label sits inside a ".field" div (for the
   auth-form's existing field spacing), and plain ".field label" (a class +
   an element = higher specificity than a single class alone) would
   otherwise win and silently override this rule's display/color/weight,
   same class of bug as the nav button fix above. */
.field-checkbox .checkbox-label {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  font-size: 13px;
  font-weight: 500;
  color: var(--text);
  cursor: pointer;
}

.field-checkbox .checkbox-label input[type="checkbox"] {
  width: auto;
  margin-top: 2px;
  flex: 0 0 auto;
}

.form-error {
  color: #c62828;
  font-size: 13px;
  min-height: 18px;
  margin: -4px 0 10px;
}

.auth-switch {
  text-align: center;
  font-size: 13.5px;
  color: var(--text-muted);
  margin-top: 16px;
}

/* Right-aligned under the password field on login.html, and reused as-is
   by forgot-password.html/reset-password.html/verify-email.html for their
   own "back to log in" links — same muted, small-print treatment as
   .auth-switch above, just right-aligned instead of centered since this one
   sits directly under a field rather than below the whole form. */
.auth-forgot {
  text-align: right;
  font-size: 13px;
  margin: -8px 0 14px;
}

/* forgot-password.html/reset-password.html/verify-email.html's success
   state — same red-for-error convention .form-error already uses, just
   green for "this worked." Not reused for .form-error itself since an
   error and a success message are never shown at the same time on these
   pages (see each page's own script), so there's no need for one class
   that switches color. */
.auth-success {
  color: #2e7d32;
  font-size: 13.5px;
  margin: 0 0 14px;
}

/* claim-account.html's own small print under its (optional, pre-filled)
   username field — same size/color/spacing as .panel .panel-hint elsewhere,
   just scoped to .field since this page's fields aren't inside a .panel. */
.field-hint {
  font-size: 13px;
  color: var(--text-muted);
  margin: 4px 0 0;
}

/* claim-account.html's migration opt-in step — its two buttons
   ("Yes, bring them over" / "No, start fresh") side by side rather than
   stacked, since neither is a full-width primary-only action the way every
   other .auth-card's single submit button is. */
.auth-actions {
  display: flex;
  gap: 10px;
  margin-top: 4px;
}
.auth-actions .btn {
  flex: 1;
}

/* ---- dashboard ---- */

.dash-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 28px 0 20px;
}

.dash-header h1 {
  font-size: 22px;
  margin: 0;
}

/* Dashboard's own two section headers (public/dashboard.html — "Your
   collections" and "Your bookmarks") reuse this same flex/space-between
   wrapper with an <h2> + a small link instead of the page's own <h1>
   above — sized down a step since each is a section heading within the
   page, not the page's own title. */
.dash-header h2 {
  font-size: 18px;
  margin: 0;
}

/* The small "Manage →"/"Browse →" links sitting in a .dash-header next to
   an <h2> above (or, for the Collection detail page's own "Manage" link —
   lib/publicPages.js's renderCollectionPage — an <h1>) — plain text links
   rather than .btn's bordered pill, so a heading stays a single compact
   line instead of a heading-sized button competing with it for space. */
.dash-header-link {
  font-size: 13.5px;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
  white-space: nowrap;
}

.dash-header-link:hover {
  text-decoration: underline;
}

/* Round 50: the "by X"/"Curated by X" byline links (lib/publicPages.js,
   public/browse.js, dashboard.js, frame.js, activity.js — all five share
   this exact `<a href="..."><strong>@username</strong></a>` shape) used to
   be bare, unclassed anchors. With nothing overriding `text-decoration`,
   they fell back to the browser's own native default underline, which
   never turns off — the one link style on the whole site that was always
   underlined at rest rather than only on hover. Every other link here
   (.comment-author, .dash-header-link above, .btn, tag/model pills, and
   the rest) explicitly sets `text-decoration: none` and adds it back only
   on :hover, so this brings the byline links in line with that same
   convention instead of leaving them on the one unstyled exception. Color
   is left alone (still the sitewide accent from the bare `a` rule) —
   only the underline behavior changes. */
.byline-author-link {
  text-decoration: none;
}

.byline-author-link:hover {
  text-decoration: underline;
}

/* Dashboard's own collapsible section headings ("Your collections"/"Your
   bookmarks" — public/dashboard.html/.js's wireSectionCollapse). The whole
   row is clickable (role="button"), not just the chevron, so it's a big
   enough target to reasonably click/tap — the nested .dash-header-link
   above stays its own separate click target underneath (dashboard.js's own
   click handler excludes it from toggling), so "Manage →"/"Browse →" still
   just navigates. */
/* Both current uses of this class (public/dashboard.html's "Your
   collections" and "Your bookmarks" section headers) sit immediately below
   something that already supplies its own breathing room above them — the
   page's own H1 .dash-header (its 20px bottom PADDING) for "Your
   collections", and the toolbar/ad-slot area's own margin for "Your
   bookmarks" — so this class's own inherited copy of .dash-header's 28px
   top padding (meant for a page-level H1 that has nothing else above it)
   was just adding an extra, unwanted 28px on top of that, which is what
   made the gap above each of these two headings noticeably bigger than the
   equivalent gap on /browse (Round 31 feedback: both "the space between
   Dashboard and Your collections" and "the Top Ad Slot spacing" read as too
   big compared to Browse — this 28px was the shared root cause behind
   both). Zeroed here rather than by overriding base .dash-header directly,
   since a bare .dash-header (a page's own H1, with nothing of its own above
   it) still wants the full 28px. See browse.css's own
   #dashboardCollectionsPanel and .ad-pos-below-toolbar comments for how
   each of the two call sites' own spacing is tuned now that this is 0. */
.dash-section-header {
  cursor: pointer;
  user-select: none;
  padding-top: 0;
}

.dash-section-header:hover h2 {
  color: var(--accent);
}

.dash-section-chevron {
  display: inline-block;
  font-size: 12px;
  color: var(--text-muted);
  margin-left: 2px;
  transition: transform 0.15s ease;
}

/* Collapsed state points the chevron sideways (▾ rotated to ▸) rather than
   swapping the glyph itself — same "one element, animate a property" the
   Mask Content toggle's own eye icon (public/app.js) already keeps simpler
   than a text/emoji swap would be. */
.dash-section-header.collapsed .dash-section-chevron {
  transform: rotate(-90deg);
}

.dash-user {
  font-size: 13.5px;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  gap: 12px;
}

.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 18px 20px;
  margin-bottom: 22px;
}

.panel h2 {
  font-size: 15px;
  margin: 0 0 6px;
}

.panel .panel-hint {
  font-size: 13px;
  color: var(--text-muted);
  margin: 0 0 14px;
}

.banner-panel {
  border-color: var(--accent);
  background: var(--accent-soft);
}

/* Dashboard's News preview (dashboard.html's #dashboardNewsPanel, up to 3
   cards, public/dashboard.js's newsCardHtml) and the full history's own
   list on /news (public/news.html/.js's own newsItemHtml, no dismiss
   button there — see that page's own comment). A .panel variant, same
   "own surface, not a full .banner-panel accent treatment" look
   .stat-box above gets — this is informational, not a call-to-action
   banner. Stacked with a small gap the same way #dashboardCollectionsList's
   own sibling sections already space themselves (.dash-header + friends),
   not a grid — one column, read top to bottom like Notifications' own
   dropdown list. */
#dashboardNewsList,
.news-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 20px;
}

.news-card {
  position: relative;
  /* Room on the right for .news-card-dismiss below, so its own long title
     never runs underneath the × — same reasoning .qb-promo-nudge-close's
     own host rule gives. */
  padding-right: 36px;
}

.news-card-dismiss {
  position: absolute;
  top: 10px;
  right: 10px;
  border: none;
  background: none;
  color: var(--text-muted);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  padding: 4px;
}

.news-card-dismiss:hover,
.news-card-dismiss:focus-visible {
  color: var(--text);
}

.news-card-title {
  margin: 0 0 6px;
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
}

.news-card-body {
  margin: 0 0 8px;
  font-size: 14px;
  color: var(--text);
  white-space: pre-wrap;
  overflow-wrap: break-word;
}

.news-card-meta {
  font-size: 12px;
  color: var(--text-muted);
  margin-bottom: 8px;
}

/* Round 50: the News item author (public/news.js + dashboard.js's shared
   newsItemHtml/newsCardHtml) is now a link to their profile, but should
   keep reading as plain byline text rather than jumping out in the
   sitewide accent color the bare `a { color: var(--accent) }` rule further
   up this file would otherwise give it — `color: inherit` picks up
   .news-card-meta's own muted grey instead. Underline only on hover/focus,
   same "not marked at rest" convention every other link on the site
   already gets from its own class (.comment-author, .dash-header-link,
   etc.) — this one just needs it spelled out explicitly since inheriting
   the parent's color means it doesn't inherit any hover behavior along
   with it. */
.news-card-author {
  color: inherit;
  text-decoration: none;
}

.news-card-author:hover,
.news-card-author:focus-visible {
  text-decoration: underline;
}

.news-card-link {
  display: inline-block;
  font-size: 13px;
  font-weight: 600;
  color: var(--accent);
  text-decoration: underline;
}

/* The full /news page's own list (public/news.html/.js) reuses
   .news-card/-title/-body/-meta/-link verbatim, just without
   .news-card-dismiss (nothing to dismiss from a permanent history page —
   see that page's own comment) and without the right-side padding
   .news-card itself would otherwise reserve for a button that never
   renders there. */
.news-list .news-card {
  padding-right: 16px;
}

/* Votes + comment count row, added alongside News votes/comments — shared
   by both the Dashboard preview (public/dashboard.js's newsCardHtml) and
   the full /news page (public/news.js's newsItemHtml). .vote-widget.horizontal
   itself is browse.css's own (loaded on both pages), so this only needs to
   lay the two pieces out next to it rather than restyle the widget. */
.news-card-footer {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-top: -4px;
}

.news-card-footer .vote-widget.horizontal {
  margin-bottom: 0;
}

/* The comment-count toggle on a News item's own footer row — shared by the
   full /news page (public/news.js's newsItemHtml) and, as of round 49, the
   Dashboard preview's own card (public/dashboard.js's newsCardHtml), both
   of which now lazily build and expand the same full thread on first click
   (public/comments.js's initComments, kind="news"). Round 49: this used to
   be the full-history page's own exclusive version of a plain, non-
   interactive `.news-card-comment-count` label the Dashboard preview kept
   instead — removed once the Dashboard preview switched to this same
   button, since nothing renders the plain label any more. Styled to read
   the same as that old plain label at rest, only visibly interactive on
   hover/focus, so it doesn't look like a stray form control sitting in a
   news card. */
.news-comment-toggle {
  font: inherit;
  font-size: 13px;
  color: var(--text-muted);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}

.news-comment-toggle:hover,
.news-comment-toggle:focus-visible {
  color: var(--accent);
}

/* Dashboard's three headline stat boxes (Views/Bookmarks/Comments — see
   dashboard.html/.js). A plain 3-up flex row of .stat-box cards, each
   built from the same background/border/radius as .panel above rather than
   its own new surface treatment, so they read as part of the same page
   rather than a visually distinct new component. Equal-width via flex: 1
   so the row always fills the container edge-to-edge regardless of digit
   count in any one box. */
.stats-row {
  display: flex;
  gap: 14px;
  margin-bottom: 22px;
}

.stat-box {
  flex: 1;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 16px 18px;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 2px;
}

.stat-value {
  font-size: 26px;
  font-weight: 700;
  line-height: 1.2;
  color: var(--text);
}

.stat-label {
  font-size: 12.5px;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* Narrow phone screens: three boxes at the desktop's roomier padding get
   cramped fast, so this drops to a single column instead of shrinking each
   box's padding/font down to illegible sizes — same breakpoint the rest of
   this app's mobile rules already use (see browse.css's own 640px queries). */
@media (max-width: 640px) {
  .stats-row {
    flex-direction: column;
  }
}

/* The full Stats page (public/stats.html/.js) — several .stats-row groups
   stacked under their own <h2>, reusing .stat-box/-value/-label as-is (same
   surface Dashboard's three headline boxes already use) rather than a new
   component, so a number reads the same everywhere in the app. Unlike the
   Dashboard's fixed 3-up row, the Bookmarks group here has six boxes, more
   than comfortably fit in one flex row at typical widths — .stats-row-wrap
   lets that one group wrap onto a second line instead of squeezing six
   boxes edge to edge, while every other (2-3 box) group stays the plain
   single-row .stats-row already used elsewhere. */
.stats-section-heading {
  margin: 28px 0 10px;
  font-size: 16px;
}

.stats-section-heading:first-of-type {
  margin-top: 0;
}

.stats-row-wrap {
  flex-wrap: wrap;
}

.stats-row-wrap .stat-box {
  /* Same minimum a box needs to keep its label from wrapping awkwardly,
     but no longer forced to exactly 1/6th of the row the way plain flex: 1
     would — three per row at typical desktop widths, fewer as it narrows. */
  flex: 1 1 200px;
}

/* A vote/comment box whose number is a combined figure across every account
   that's bookmarked the same URL (see routes/account.js's own GET
   /stats/detailed comment) — same muted, de-emphasized treatment
   .row-field-empty already gives a secondary annotation elsewhere, just
   inline within the label instead of its own line. */
.stat-note {
  display: block;
  font-size: 10.5px;
  text-transform: none;
  letter-spacing: normal;
  color: var(--text-muted);
}

/* Admin page — one .admin-slot-card (a .panel) per registered ad slot, see
   public/admin.js's slotCardHtml(), now grouped under a per-page <h2>
   (pageSectionHtml/renderSlots) the same way the Ad Rotator tab's own
   rotatorSizeSectionHtml groups creatives by size — a .panel of .panel
   cards, nested. A card's own heading dropped from <h2> to <h3> now that
   it sits one level under that page heading (its text also lost the page
   name prefix, since the section heading already says it — see
   slotCardHtml's own comment); this rule keeps it the same size/spacing
   as .panel h2 above rather than falling back to the browser's own, much
   larger default h3 styling. */
.admin-slot-card h3 {
  font-size: 15px;
  margin: 0 0 6px;
}

.admin-embed-input {
  width: 100%;
  box-sizing: border-box;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  resize: vertical;
  margin-bottom: 10px;
}

.admin-embed-input:focus {
  outline: none;
  border-color: var(--accent);
}

.admin-slot-toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13.5px;
  font-weight: 500;
  cursor: pointer;
  margin-bottom: 14px;
}

.admin-slot-toggle input[type="checkbox"] {
  width: auto;
}

.admin-slot-row {
  display: flex;
  align-items: center;
  gap: 12px;
}

.admin-slot-status {
  font-size: 13px;
}

.admin-slot-status-ok {
  color: var(--accent);
}

.admin-slot-status-error {
  color: #c62828;
}

.admin-slot-updated {
  font-size: 12px;
  color: var(--text-muted);
  margin: 10px 0 0;
}

/* Admin page — the ad-slot card's Fallback URL field (public/admin.js's
   slotCardHtml, GET /api/public/fallback.js in routes/public.js). Sits
   below .admin-slot-updated with its own top border since it's a separate,
   read-only, nothing-to-save concern from the config fields above it in
   the same card. */
.admin-slot-fallback {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

.admin-slot-fallback-label {
  margin: 0 0 8px;
  font-size: 13.5px;
  font-weight: 500;
}

.admin-slot-fallback-row {
  display: flex;
  gap: 8px;
}

.admin-slot-fallback-input {
  flex: 1;
  min-width: 0;
  box-sizing: border-box;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text-muted);
  font: 12.5px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

.admin-slot-fallback-input:focus {
  outline: none;
  border-color: var(--accent);
  color: var(--text);
}

/* Admin page — the ad-slot card's "Custom code" vs "Ad Rotator" radio pair
   (public/admin.js's slotCardHtml) and the size dropdown that appears when
   Rotator is picked. The two radio labels reuse .admin-slot-toggle for
   their own icon+text+cursor styling (same class the plain Enabled/Full
   width checkboxes above use) — this wraps THEM in a row together instead
   of the vertical stack .admin-slot-toggle's own display:flex would give
   two of them back to back with nothing else grouping them. */
.admin-slot-mode {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 20px;
  margin-bottom: 14px;
}

.admin-slot-mode .admin-slot-toggle {
  margin-bottom: 0;
}

.admin-slot-mode input[type="radio"] {
  width: auto;
}

.admin-slot-embed-field,
.admin-slot-rotator-field {
  margin-bottom: 0;
}

.admin-slot-rotator-field select {
  width: 100%;
  box-sizing: border-box;
  padding: 9px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font-size: 14px;
  margin-bottom: 10px;
}

.admin-slot-rotator-field select:focus {
  outline: none;
  border-color: var(--accent);
}

/* Admin page — the Ad Rotator tab, one .admin-creative-card (a .panel) per
   creative in a size's pool (public/admin.js's creativeCardHtml). Reuses
   .admin-embed-input/.admin-slot-toggle/.admin-slot-row/.admin-slot-status
   from the ad-slot cards above rather than its own copies — same textarea,
   Enabled checkbox, and Save-button-plus-status-message shape, just one
   level smaller (a creative, not a whole slot). margin-bottom: 0 on the
   card itself hands all the spacing between cards to the list's own gap
   instead of doubling up with .panel's normal 22px. */
.admin-creatives-list {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-bottom: 14px;
}

.admin-creative-card {
  margin-bottom: 0;
}

/* Admin page — the Users panel's search box, styled the same as every
   other page's own search input (.toolbar input[type="text"] above) even
   though this one isn't inside a .toolbar — the panel here only ever has
   this one control, so the flex row .toolbar exists for isn't needed. */
#userSearch {
  width: 100%;
  box-sizing: border-box;
  padding: 9px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font-size: 14px;
}

#userSearch:focus {
  outline: none;
  border-color: var(--accent);
}

/* Admin page — the Featured Collections panel's search box (#featuredSearch)
   — same exact rule as #userSearch above (this app's established pattern
   for an admin panel's own single search input: match the main site's
   input styling by copying the rule under this control's own id, per the
   comment on #userSearch). Previously had no CSS at all, so it rendered as
   an unstyled native browser input, out of place next to the rest of the
   admin UI's styled controls. */
#featuredSearch {
  width: 100%;
  box-sizing: border-box;
  padding: 9px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font-size: 14px;
}

#featuredSearch:focus {
  outline: none;
  border-color: var(--accent);
}

/* Admin page — the Tags panel's "block a new tag" form (#addBlockedTagForm)
   — a plain text input plus its Add-shaped submit button on one row, same
   general shape as #userSearch above just with a button attached rather
   than filtering as-you-type. Previously only had the flex-layout rule
   below (sizing within the row) with no visual box styling at all, so
   #addBlockedTagInput rendered as an unstyled native input — the second
   rule here gives it the same border/padding/radius/background/focus
   treatment as #userSearch and every other text input on the main site. */
.admin-inline-form {
  display: flex;
  gap: 8px;
  align-items: center;
}

.admin-inline-form input[type="text"] {
  flex: 1 1 auto;
  min-width: 0;
  box-sizing: border-box;
  padding: 9px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font-size: 14px;
}

.admin-inline-form input[type="text"]:focus {
  outline: none;
  border-color: var(--accent);
}

/* One .admin-blocked-tag row per lib/blockedTags.js entry — same pill shape
   as .tag-pill below (background/radius/padding/font-size), so a blocked
   tag still reads visually as "this is a tag" the same way it does
   everywhere else on the site, just as its own plain <span> (not a link —
   nothing to click through to here) with a small × remove button riding
   along inside the same pill instead of a separate Delete button off to the
   side, which there's nothing else on this row to separate it from. */
.admin-blocked-tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--accent-soft);
  color: var(--accent-dark);
  border-radius: 999px;
  padding: 3px 6px 3px 10px;
  font-size: 12px;
  font-weight: 600;
  margin: 2px 8px 8px 0;
}

.admin-blocked-tag-remove {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-muted);
  font-size: 1.1em;
  line-height: 1;
  padding: 0;
}

.admin-blocked-tag-remove:hover {
  color: var(--text);
}

/* Admin page — the Users panel, one .admin-user-card (a .panel) per
   matching account — see public/admin.js's userCardHtml(). Same
   .admin-slot-toggle class as the ad-slot cards' own Enabled checkbox
   (above) for the "Ads enabled" one here, just laid out on the opposite
   end of the row instead of stacked underneath, since there's nothing else
   on this card to stack it under. */
.admin-user-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 14px 18px;
}

.admin-user-info {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.admin-user-info .text-muted {
  font-size: 13px;
}

/* A Partner's own optional `title` (lib/partners.js, migration 043) shown
   as a third muted line under its name/URL — italic distinguishes it at a
   glance from the plain URL line just above, which shares the same
   .text-muted base size/color. */
.admin-partner-title {
  font-style: italic;
}

/* Admin > Users' confirmed/unconfirmed marker next to each account's email
   (public/admin.js's emailStatusIconHtml) — same small circular
   Unicode-glyph badge shape as .member-check further down this file (plain
   circle + character, not an SVG, for the same "trivial to reproduce from
   plain template-string HTML" reason that one's own comment gives), just
   its own two-state color/glyph pair instead of that one's single pink
   checkmark. */
.email-status-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  margin-left: 5px;
  border-radius: 50%;
  color: #fff;
  font-size: 9px;
  line-height: 1;
  vertical-align: middle;
  flex-shrink: 0;
}

.email-status-icon.confirmed {
  background: #2e7d32;
}

.email-status-icon.unconfirmed {
  background: #9e9e9e;
}

/* Admin > Users' read-only "Admin" badge (public/admin.js's userCardHtml)
   — same pill shape as .collection-badge elsewhere on this page, its own
   class since neither of that one's public/private colors fit "this
   account itself is an admin." */
.admin-user-badge {
  display: inline-block;
  margin-left: 8px;
  font-size: 11px;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 999px;
  color: var(--accent);
  background: var(--accent-soft);
  vertical-align: middle;
}

.admin-user-card .admin-slot-toggle {
  margin-bottom: 0;
  white-space: nowrap;
}

/* Paid Membership's Grant/Revoke row (public/admin.js's membershipBlockHtml)
   — flex-basis: 100% drops it onto its own line below .admin-user-info/
   .admin-slot-toggle within the card's own flex-wrap row above, rather than
   fighting those two for space on the same line. */
.admin-user-membership {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  flex-basis: 100%;
}

/* Admin page — the Legacy URLs panel, one .admin-legacy-url-card (a .panel)
   per distinct path+query, built by public/admin.js's
   legacyUrlHitCardHtml(). Same flex-row shape as .admin-user-card above,
   just with a hit-count badge in place of that card's toggle/actions. */
.admin-legacy-url-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 14px 18px;
}

/* Admin > Partners, one .admin-partner-row (a .panel) per lib/partners.js
   entry — public/admin.js's partnerHtml(). Same flex-row shape as
   .admin-user-card/.admin-legacy-url-card above (name+URL grouped on the
   left via the shared .admin-user-info), plus a .drag-handle (this file's
   own .drag-handle rule below, same one Featured Collections/Collection
   Manage already use) and an Enabled/Disabled toggle alongside Edit/Remove
   in .admin-model-actions. */
.admin-partner-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 14px 18px;
  margin-bottom: 10px;
}

/* Same dimmed-while-dragging feedback as .featured-collection-row.dragging/
   .collection-manage-row.dragging below — public/admin.js's
   wireDragAndDropPartners/wireDragAndDropSiteOverrides toggle this class on
   the one row actually being dragged. */
.admin-partner-row.dragging,
.admin-site-override-row.dragging {
  opacity: 0.4;
}

/* Admin > Legacy URLs, one .admin-legacy-redirect-row (a .panel) per
   lib/legacyUrlRedirects.js entry — public/admin.js's legacyRedirectHtml().
   Same flex-row shape as .admin-legacy-url-card/.admin-partner-row above
   (old path + arrow + new path grouped on the left via the shared
   .admin-legacy-url-info, a single Remove button on the right). */
.admin-legacy-redirect-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 14px 18px;
}

/* Admin > Models, one .admin-model-row (a .panel) per lib/models.js entry —
   public/admin.js's modelCardHtml(). Same flex-row shape as
   .admin-legacy-redirect-row just above (reuses its own
   .admin-legacy-url-info for the name+meta grouping on the left), but with
   TWO actions on the right (Approve/Unapprove and Remove, not just one) —
   .admin-model-actions groups those so they wrap together as a pair rather
   than independently if the row gets too narrow. */
.admin-model-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 14px 18px;
}

.admin-model-actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}

/* Admin > Site name fallbacks, one .admin-site-override-row (a .panel) per
   lib/siteNameOverrides.js entry — public/admin.js's siteOverrideHtml().
   Same flex-row shape, drag handle, and Enabled/Disabled toggle as
   .admin-partner-row just above (kept even though the public
   /partners#sites section this drag/toggle UI used to control has since
   been removed — see lib/siteNameOverrides.js's own top comment). */
.admin-site-override-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 14px 18px;
  margin-bottom: 10px;
}

/* Admin > Legacy Users (Round 9) — the Import/Send-all buttons sit side by
   side above the row list, same reasoning as .auth-actions' two buttons on
   claim-account.html. `flex-wrap` added once a third button (Send
   reminder, this round) made three buttons wide enough to overflow a
   narrower admin viewport — wraps to a second line instead of scrolling. */
.admin-legacy-users-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 4px 0 10px;
}

/* The inline typed-confirmation step "Send to all pending" reveals —
   .admin-inline-form (above) supplies its input+button row; this just adds
   the same .panel-like soft inset the rest of this page uses to set an
   in-progress, not-yet-committed action visually apart from the page around
   it, without introducing an actual nested .panel (which would double up
   the card border already around the whole section). */
.admin-legacy-users-confirm {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 14px 16px;
  margin: 4px 0 10px;
}

.admin-legacy-users-confirm p {
  margin: 0 0 10px;
  font-size: 13.5px;
}

/* One .admin-legacy-user-row (a .panel) per legacy_users row — same
   flex-row shape as .admin-site-override-row just above (reuses
   .admin-user-info for the email/username grouping), plus its own status
   badge and a small inline "send test" form that doesn't need its own
   dedicated row-width treatment since it's just one text input + one
   button, same size class as .admin-inline-form's. */
.admin-legacy-user-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 14px 18px;
  margin-bottom: 10px;
}

.admin-legacy-user-counts {
  flex-shrink: 0;
  font-size: 13px;
  color: var(--text-muted);
}

.admin-legacy-user-status {
  display: inline-block;
  font-size: 11px;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 999px;
  color: var(--text-muted);
  background: var(--bg);
  border: 1px solid var(--border);
  white-space: nowrap;
}

.admin-legacy-user-status-migrated {
  color: #2e7d32;
  background: rgba(46, 125, 50, 0.12);
  border-color: transparent;
}

.admin-legacy-user-status-claimed {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: transparent;
}

/* Round 20 — a legacy user who unsubscribed. Muted/neutral, not the
   red/danger tone something failed would get: opting out isn't an error
   state, it's a respected request, same reasoning the plain (no color
   class) "Invited"/"Not yet emailed" badges above already treat as
   unremarkable. */
.admin-legacy-user-status-opted-out {
  color: var(--text-muted);
  background: rgba(120, 120, 120, 0.12);
  border-color: transparent;
}

/* Later round — SendGrid itself reported this row's own email as bounced,
   blocked, or invalid (lib/legacyDeliveryStatus.js). Same soft-tint shape
   as -migrated/-claimed above, but off .btn-danger's own red (#c62828,
   see that class) rather than inventing a new color — this is the one
   status here that represents an actual problem worth an admin's
   attention, same reasoning that red already carries elsewhere on this
   page (e.g. the "Send reminder" confirmation's own danger button). */
.admin-legacy-user-status-delivery-bad {
  color: #c62828;
  background: rgba(198, 40, 40, 0.1);
  border-color: transparent;
}

.admin-legacy-user-send-test {
  display: flex;
  gap: 6px;
  align-items: center;
  flex-shrink: 0;
}

.admin-legacy-user-send-test input[type="email"] {
  width: 200px;
  padding: 7px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font-size: 13px;
}

/* Admin-assisted merge (later round) — step 1, the lookup form, is the
   same size class as .admin-legacy-user-send-test just above. Step 2, the
   confirmation it reveals in its place, is a separate element (see
   legacyUserRowHtml's own comment) rather than a second state of this same
   one, so it gets its own flex-basis: 100% to take a full row of its own
   inside .admin-legacy-user-row's flex-wrap — its preview text runs too
   long to share a line with the row's other badges/buttons. */
.admin-legacy-user-merge-lookup {
  display: flex;
  gap: 6px;
  align-items: center;
  flex-shrink: 0;
}

.admin-legacy-user-merge-lookup input[type="email"] {
  width: 200px;
  padding: 7px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font-size: 13px;
}

.admin-legacy-user-merge-confirm {
  flex-basis: 100%;
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 12px 14px;
}

.admin-legacy-user-merge-confirm-text {
  margin: 0;
  font-size: 13px;
  flex: 1 1 260px;
}

/* Impersonate User (later round) — one .admin-impersonation-log-row per
   session, built by public/admin.js's impersonationLogRowHtml(). A plain
   read-only line reusing .admin-user-info's own text stack (see that class
   above) — nothing in a log row is ever clicked, so it only needs its own
   tighter padding, not a whole second layout. */
.admin-impersonation-log-row {
  padding: 12px 18px;
  margin-bottom: 8px;
}

.admin-legacy-url-info {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  word-break: break-all;
}

/* Groups a Model's name + its new "Edit" button (round-32 punchlist) as one
   inline row, rather than each becoming its own stacked line the way plain
   flex-column children of .admin-legacy-url-info above otherwise would —
   same "title + inline control" grouping instinct .collection-card-title
   already applies elsewhere for a name sitting next to a badge. */
.admin-model-name-line {
  display: flex;
  align-items: center;
  gap: 8px;
}

.admin-legacy-url-info .text-muted {
  font-size: 13px;
  word-break: break-word;
}

.admin-legacy-url-count {
  flex-shrink: 0;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-muted);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 4px 12px;
  white-space: nowrap;
}

.admin-user-membership .btn {
  padding: 4px 10px;
  font-size: 13px;
}

/* Account Settings page (public/account.html/js). .admin-slot-toggle
   (above) is reused as-is for the Public profile checkbox; these are the
   two pieces that page needs of its own. */
.account-radio-group {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 6px;
}

.account-radio {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13.5px;
  font-weight: 500;
  cursor: pointer;
}

.account-radio input[type="radio"] {
  width: auto;
}

.account-setting-status {
  font-size: 13px;
  min-height: 16px;
}

.account-setting-status-ok {
  color: var(--accent);
}

.account-setting-status-error {
  color: #c62828;
}

/* Account Settings' Email panel — the Verified/Not verified badge next to
   the address itself. Same warm-red-for-a-problem convention
   .row-dead-link already uses for a broken bookmark link, and the same
   accent-green .auth-success uses for "this worked" — reused colors, not
   new ones, so this doesn't introduce a third meaning for either hue. */
.verified-badge-yes {
  color: #2e7d32;
  font-weight: 600;
}

.verified-badge-no {
  color: #c62828;
  font-weight: 600;
}

/* Paid Membership's pink checkmark — rendered next to a username wherever
   it appears as a link to that account (profile page heading, bookmark
   listing bylines, the bookmark permalink page's byline, comment authors,
   and the signed-in visitor's own nav identity), backed by every public
   API response's `is_member`/`isMember` field (see lib/publicBookmarks.js's
   PUBLIC_FIELDS and lib/comments.js's IS_MEMBER_SQL, both intentionally
   mirroring lib/membership.js's isActive()). Deliberately a fixed pink,
   independent of --accent/THEME_COLOR — the whole point (X.com-style) is a
   consistent, recognizable "this account is verified/paying" color that
   doesn't shift with a white-labeled deployment's own branding. Built with
   a plain circle + Unicode checkmark rather than an SVG so this same tiny
   snippet is trivial to reproduce identically from both server-rendered
   HTML (lib/publicPages.js) and client-side script (public/app.js,
   public/browse.js, public/comments.js) without sharing a build step. */
.member-check {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 15px;
  height: 15px;
  margin-left: 3px;
  border-radius: 50%;
  background: #ec2899;
  color: #fff;
  font-size: 10px;
  line-height: 1;
  vertical-align: middle;
  flex-shrink: 0;
}

/* The profile page's own "Member" pill (public/browse.js's profileBadge) —
   distinct from .member-check above: this is a standalone, textual badge
   shown once in the profile-meta row (alongside "Joined <Month Year>" and
   follower/following counts), not a small mark repeated next to every
   occurrence of the name. Same pink as .member-check, so the two read as
   "the same feature" despite the different shape. */
.member-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 10px;
  border-radius: 999px;
  background: #ec2899;
  color: #fff;
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
}

/* Account Settings' Cancel Account panel — the one destructive action on
   this page, so it gets a visually distinct treatment from every other
   .panel here (a muted red border instead of the default neutral one)
   purely so a skimming visitor's eye catches "this one's different" before
   reading a word of it, the same reason a browser's own "delete" dialogs
   are rarely styled identically to an "OK" one. */
.panel-danger {
  border-color: #c62828;
}

.panel-danger .field {
  max-width: 320px;
}

.btn-danger {
  background: #c62828;
  border-color: #c62828;
  color: #fff;
}

.btn-danger:hover {
  background: #a92121;
  border-color: #a92121;
  color: #fff;
}

/* Admin page — the two-column layout: a vertical tab list down the left
   (.admin-tabs) and the active panel on the right (.admin-panels), wrapping
   both (public/admin.html's #adminContent). Used to be a single horizontal
   row of tabs sitting above the panels, which read fine back when there
   were two or three of them, but at nine it wrapped onto a second line
   even on an ordinary desktop width — a sidebar list scales to more tabs
   without ever wrapping, and reads as a proper section list (like a
   settings page) rather than a toolbar. align-items: flex-start keeps the
   sidebar from stretching to the content panel's height when one panel is
   much taller than the tab list itself. */
.admin-layout {
  display: flex;
  align-items: flex-start;
  gap: 28px;
}

.admin-panels {
  flex: 1;
  min-width: 0; /* lets long content (e.g. a wide table) shrink instead of pushing the sidebar out */
}

/* The tab list itself (public/admin.js's tab-click listener, delegated on
   this container — unchanged by this vertical layout, it only cares about
   .admin-tabs/.admin-tab/data-tab, never their arrangement). Plain buttons,
   not real `<a href>` tabs — there's nothing to deep-link to, every panel's
   markup already loads together, this is purely a same-page show/hide
   toggle. */
.admin-tabs {
  display: flex;
  flex-direction: column;
  flex: 0 0 200px;
  gap: 2px;
  border-bottom: none;
  border-right: 1px solid var(--border);
  margin-bottom: 0;
  padding-right: 10px;
}

.admin-tab {
  padding: 10px 14px;
  border: none;
  background: none;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  color: var(--text-muted);
  cursor: pointer;
  text-align: left;
  border-radius: 6px;
  border-left: 2px solid transparent;
}

.admin-tab:hover {
  color: var(--text);
  background: var(--bg);
}

.admin-tab-active {
  color: var(--accent);
  background: var(--accent-soft);
  border-left-color: var(--accent);
}

/* Below 640px there isn't room for a sidebar next to a content panel — the
   same breakpoint every other phone-width layout change in this app uses
   (see browse.css's own .type-filter-wrap/.view-toggle rules). The tab list
   goes back to a single horizontal, side-scrolling row above the panel
   (rather than stacking nine full-width rows, which would push the actual
   panel below a very long list of buttons), and the active-tab highlight
   switches from a left border (meaningless once the list isn't vertical)
   to the same underline style the old horizontal tab bar used. */
@media (max-width: 640px) {
  .admin-layout {
    flex-direction: column;
    gap: 16px;
  }

  .admin-tabs {
    flex-direction: row;
    flex: 0 0 auto;
    width: 100%;
    overflow-x: auto;
    border-right: none;
    border-bottom: 1px solid var(--border);
    padding-right: 0;
    padding-bottom: 6px;
  }

  .admin-tab {
    flex: 0 0 auto;
    white-space: nowrap;
    border-left: none;
    border-radius: 6px 6px 0 0;
  }

  .admin-tab-active {
    border-left-color: transparent;
    border-bottom: 2px solid var(--accent);
  }
}

/* Admin page — the Reports panel, one .report-card (a .panel) per open (or,
   after the "Show resolved" toggle, resolved) report — see
   public/admin.js's reportCardHtml(). */
.report-card {
  padding: 14px 18px;
}

/* A report flagging content as involving a minor gets a visibly different
   treatment from the rest of the queue — a left accent bar in a distinct
   color, not just the same badge everything else gets — so it can't get
   lost between a run of "Broken or doesn't work" reports waiting their
   turn. This is a queue-ordering aid for an admin, not a moderation
   decision on its own — see lib/reports.js's own comment on why this
   reason exists. */
.report-card-urgent {
  border-left: 3px solid #c62828;
}

.report-card-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
}

.report-reason-badge {
  display: inline-block;
  font-size: 12px;
  font-weight: 600;
  padding: 3px 10px;
  border-radius: 100px;
  background: var(--accent-soft);
  color: var(--accent);
  white-space: nowrap;
}

.report-card-urgent .report-reason-badge {
  background: #fdeaea;
  color: #c62828;
}

.report-meta {
  font-size: 13px;
  color: var(--text-muted);
}

.report-note {
  font-size: 13.5px;
  margin: 0 0 12px;
  padding: 10px 12px;
  background: var(--bg);
  border-radius: 8px;
}

.report-card-actions {
  display: flex;
  align-items: center;
}

.report-resolved-label {
  font-size: 13px;
  color: var(--text-muted);
}

/* Admin page — the Moderation panel's own case card (a .panel, same
   .report-card shape reused from the Reports panel just above it —
   public/admin.js's moderationCaseCardHtml()) and its collapsible thread
   underneath (.moderation-thread, toggled by "View thread"). */
.moderation-thread {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

/* One .report-note-styled bubble per message, just with a left accent bar
   that differs by sender — same "distinct border color, not a distinct
   shape" treatment .report-card-urgent's own comment above already uses to
   tell two kinds of card apart at a glance. */
.moderation-message {
  font-size: 13.5px;
  margin: 0 0 8px;
  padding: 8px 12px;
  background: var(--bg);
  border-radius: 8px;
  border-left: 3px solid var(--border);
}

.moderation-message-admin {
  border-left-color: var(--accent);
}

.moderation-message-sender {
  display: block;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--text-muted);
  margin-bottom: 2px;
}

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

.moderation-compose textarea {
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  resize: vertical;
}

.moderation-ban-field {
  margin-top: 10px;
}

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

.username-form {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

.username-form input {
  flex: 1 1 220px;
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 14px;
  font-family: inherit;
}

.username-form input:focus {
  outline: none;
  border-color: var(--accent);
}

.bookmarklet-row {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}

.bookmarklet-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--accent-soft);
  color: var(--accent-dark);
  border: 1px dashed var(--accent);
  border-radius: 8px;
  padding: 9px 16px;
  font-size: 13.5px;
  font-weight: 700;
  text-decoration: none;
  cursor: grab;
  user-select: none;
}

.bookmarklet-regenerate {
  font-size: 12.5px;
  color: var(--text-muted);
  background: none;
  border: none;
  cursor: pointer;
  text-decoration: underline;
  font-family: inherit;
  padding: 0;
}

.toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 16px;
}

.toolbar input[type="text"] {
  flex: 1;
  padding: 9px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 14px;
}

.toolbar input[type="text"]:focus {
  outline: none;
  border-color: var(--accent);
}

/* Activity page's three modules (public/activity.html/js) — a plain button
   row, not a full tab widget: activity.js toggles which .activity-panel is
   hidden and which button carries aria-selected="true", styled here purely
   off that attribute rather than a separate "active" class so the two can
   never drift out of sync with each other. */
.activity-tabs {
  display: flex;
  gap: 8px;
  margin-bottom: 16px;
  border-bottom: 1px solid var(--border);
}

.activity-tab {
  padding: 10px 16px;
  border: none;
  border-bottom: 2px solid transparent;
  background: none;
  color: var(--text-muted);
  font-size: 14px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
}

.activity-tab:hover {
  color: var(--text);
}

.activity-tab[aria-selected="true"] {
  color: var(--text);
  border-bottom-color: var(--accent);
}

.count {
  color: var(--text-muted);
  font-size: 13px;
  margin-bottom: 12px;
}

.list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.row {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 12px 14px;
  display: flex;
  align-items: flex-start;
  gap: 10px;
}

.row-main {
  flex: 1;
  min-width: 0;
}

.row-title-line {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

.row-favicon {
  width: 16px;
  height: 16px;
  border-radius: 3px;
  flex: 0 0 auto;
}

.row-title {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.row-title:hover {
  text-decoration: underline;
}

.row-sitename {
  display: inline-block;
  font-size: 11.5px;
  color: var(--text-muted);
  background: var(--accent-soft);
  padding: 1px 6px;
  border-radius: 999px;
  flex: 0 0 auto;
  text-decoration: none;
}

/* The Sites feature (migration 063) turned this into a real <a href="/site/
   NAME"> link everywhere it's shown on a PUBLIC surface (public/browse.js's
   rows, public/frame.js's toolbar-viewer card, lib/publicPages.js's
   permalink/collection-row renders, public/collectionDetail.js) — this
   :hover only ever shows on those, matching .tag-pill/.model-pill's own
   hover treatment just below. Still a plain, non-interactive <span> on the
   Dashboard and Activity pages (public/dashboard.js, public/activity.js),
   same "no public link from a private/personal surface" precedent those two
   pages' own tag/model pills already set — this rule simply never matches
   there. */
.row-sitename:hover {
  background: var(--accent);
  color: var(--accent-contrast);
}

/* View counter — shown on the dashboard's own rows (next to the title,
   since there's no meta/date line there to fold into) and on the public
   /browse row list and /bookmark/:id permalink page (browse.css's
   .row-meta/.bm-meta, which this inherits its muted/small look from). See
   lib/views.js for why this is per-bookmark, not shared across every
   bookmark row for the same URL the way vote scores are. */
.row-views {
  font-size: 11.5px;
  color: var(--text-muted);
  flex: 0 0 auto;
  white-space: nowrap;
}

/* Dashboard-only click-through to a bookmark's public /bookmark/:id
   permalink (public/dashboard.js) — only ever rendered for a row that's
   currently public AND not flagged as a dead link (see .row-dead-link
   below), so there's no disabled/broken-link state to style here. Small and
   muted like .row-views right before it, not accent-colored like
   .row-title — this is a secondary path off the row, not its main action
   (that's still the title/thumbnail's own /view/:id link). */
.row-permalink {
  font-size: 11.5px;
  color: var(--text-muted);
  text-decoration: none;
  white-space: nowrap;
  flex: 0 0 auto;
}

.row-permalink:hover {
  color: var(--accent);
  text-decoration: underline;
}

/* Dashboard-only "this URL's last check came back 404" indicator
   (public/dashboard.js, lib/urls.js's is_dead_link) — a fixed warm-red tint
   regardless of theme (not var(--accent), which itself flips between green
   and pink across this app's own theme variants — a warning shouldn't ride
   along with a color that isn't reliably read as one) so it reads as a
   caution state at a glance rather than blending in with this row's other
   muted metadata. Never shown for a row that isn't currently flagged dead —
   see dashboard.js's own comment on why. */
.row-dead-link {
  font-size: 11.5px;
  font-weight: 600;
  color: #c62828;
  background: rgba(198, 40, 40, 0.12);
  padding: 1px 7px;
  border-radius: 999px;
  white-space: nowrap;
  flex: 0 0 auto;
}

/* Dashboard-only "an admin hid this bookmark's video embed" indicator
   (public/dashboard.js, migration 031's urls.video_hidden) — same pill
   shape/size/weight as .row-dead-link just above (deliberately similar,
   per the request that led to this: "a better indicator for hidden videos,
   similar to the Broken video example"), but its own fixed amber tone
   rather than that class's warm red — a dead link is an accident (a page
   404ing on its own), this is a deliberate moderation action, and the two
   reading as visually distinct at a glance matters as much as either being
   visible at all. Fixed regardless of theme, same reasoning as
   .row-dead-link's own comment: a caution color needs to stay reliably
   readable as one, not ride along with var(--accent)'s own green/pink
   theme swap. */
.row-video-hidden {
  font-size: 11.5px;
  font-weight: 600;
  color: #e65100;
  background: rgba(230, 81, 0, 0.12);
  padding: 1px 7px;
  border-radius: 999px;
  white-space: nowrap;
  flex: 0 0 auto;
}

/* Same small-text-button look as .row-field-edit right below, so this reads
   as "one more small inline action on the row" rather than a visually
   distinct new kind of control. */
.row-recheck-link {
  background: none;
  border: none;
  color: var(--accent);
  font-size: 11.5px;
  font-weight: 600;
  cursor: pointer;
  padding: 0;
  font-family: inherit;
  white-space: nowrap;
  flex: 0 0 auto;
}

.row-recheck-link:hover {
  text-decoration: underline;
}

.row-recheck-link:disabled {
  color: var(--text-muted);
  cursor: default;
  text-decoration: none;
}

.row-url-line {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  margin: 2px 0 8px;
}

.row-url {
  font-size: 12px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.row-description {
  font-size: 12.5px;
  color: var(--text-muted);
  margin: 0 0 4px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Applied to a .row-description by initDescriptionToggles (its own small
   copy in public/browse.js, dashboard.js, collectionDetail.js, and
   activity.js — see browse.css's own comment by .tag-toggle for why each
   file keeps its own copy rather than a shared module) when its "…more"
   button is clicked — just lifts the 2-line clamp above back off so the
   full text flows. */
.row-description-expanded {
  -webkit-line-clamp: unset;
  overflow: visible;
}

/* The "…more"/"Less" button initDescriptionToggles appends right after a
   .row-description, but ONLY when that row's text actually overflows two
   lines — a short description never gets a toggle at all. Text-link style
   rather than .tag-toggle's pill, since this sits alone under a paragraph
   rather than inline among other pills. */
.description-toggle {
  display: block;
  background: none;
  border: none;
  color: var(--accent);
  font-size: 12.5px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  padding: 0;
  margin: 0 0 8px;
}

.description-toggle:hover {
  text-decoration: underline;
}

.row-thumb {
  width: 72px;
  height: 72px;
  object-fit: cover;
  border-radius: 8px;
  flex: 0 0 auto;
  border: 1px solid var(--border);
}

.row-tags,
.row-models,
.row-notes {
  width: 100%;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 5px 8px;
  font-size: 12.5px;
  font-family: inherit;
  margin-top: 4px;
  resize: vertical;
}

.row-tags:focus,
.row-models:focus,
.row-notes:focus {
  outline: none;
  border-color: var(--accent);
}

/* Wraps just the Models textarea (not the whole .row-field-models block) so
   .row-models-suggestions below can position itself directly under the
   textarea alone, regardless of where the field's label/display/Edit
   button above it sit. See public/dashboard.js's wireModelsAutocomplete. */
.row-models-input-wrap {
  position: relative;
}

/* Approved-name autocomplete dropdown for the Models field — same
   floating-panel shape as .notif-dropdown (surface/border/shadow), just
   anchored under a textarea instead of a nav icon. Only ever populated
   with APPROVED names (lib/models.js's listApprovedModelNames) — the ★
   prefix on each suggestion (dashboard.js's renderSuggestions) matches
   .model-pill-approved's own ★ elsewhere, so a suggestion visibly promises
   "picking this gets you the star immediately," not just "this exists." */
.row-models-suggestions {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: 2px;
  max-height: 180px;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
  z-index: 20;
}

.row-models-suggestion {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 6px 10px;
  font-size: 12.5px;
  font-family: inherit;
  color: var(--model-gold);
  cursor: pointer;
}

.row-models-suggestion:hover,
.row-models-suggestion-active {
  background: var(--model-gold-soft);
}

/* Wrapper divs anchoring the mention-autocomplete dropdown (typed while
   composing, not the read-only mention-link rendering that .mention-body
   elsewhere is for) directly under each #tag/@username/*model/^site-capable
   free-text field: the comment box, the dashboard's own Notes field, and
   Admin's News Title/Body. Same reasoning as .row-models-input-wrap just
   above, just shared across every field that has this feature instead of
   only Models. See public/comments.js's, public/dashboard.js's, and
   public/admin.js's own wireMentionAutocomplete. */
.comment-input-wrap,
.row-notes-input-wrap,
.admin-news-title-wrap,
.admin-news-body-wrap {
  position: relative;
}

/* The mention-autocomplete dropdown itself — same floating-panel shape as
   .row-models-suggestions just above, minus that field's own
   ★-approved-only styling (a mention suggestion here can be any of four
   different kinds, not just Models, and only ever promises "this exists,"
   not "picking this gets you a star"). */
.mention-suggestions {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: 2px;
  max-height: 180px;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
  z-index: 20;
}

.mention-suggestion {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 6px 10px;
  font-size: 12.5px;
  font-family: inherit;
  color: var(--accent);
  cursor: pointer;
}

.mention-suggestion:hover,
.mention-suggestion-active {
  background: var(--accent-soft);
}

/* .tag-pill lives here (not browse.css) because the dashboard's read/edit
   tag display (server/public/dashboard.js) reuses the exact same look as
   the public pages' clickable tag pills — both are links now: the public
   ones go to /tag/NAME, the dashboard's own filter its own list in place
   (see dashboard.js's setActiveTag) since a dashboard row can be private
   and has no public /tag/NAME page to send you to. One visual style,
   shared by both contexts. */
.tag-pill {
  display: inline-block;
  background: var(--accent-soft);
  color: var(--accent-dark);
  border-radius: 999px;
  padding: 3px 10px;
  font-size: 12px;
  font-weight: 600;
  text-decoration: none;
  margin: 2px 4px 2px 0;
}

.tag-pill:hover {
  background: var(--accent);
  color: var(--accent-contrast);
}

/* .model-pill — a "Model" credit chip (lib/models.js), same base shape as
   .tag-pill just above, and the same "both are links, just to different
   places" split too: public/browse.js's buildModelsHtml and
   lib/publicPages.js's buildRowModelsHtml render this as an <a> to
   /model/NAME, while the Dashboard's own copy (public/dashboard.js's
   renderModelPills) filters its own list in place instead (setActiveModel)
   — same reasoning as .tag-pill's own comment, a dashboard row can be
   private and has no public /model/NAME page to send you to. Shown for
   BOTH an approved and a still-pending name (a brand-new Model credit is
   usable immediately, just unbadged — see migration 041's own comment on
   that choice) — .model-pill-approved is the only difference, adding the ★
   and swapping in the gold/amber pair (--model-gold/--model-gold-soft) once
   an admin has approved the name from the Admin "Models" tab. Approved or
   not, the pill always links/filters through either way — /model/:name
   itself doesn't gate on is_approved either (see getPublicModelCounts' own
   comment in lib/publicBookmarks.js for why only the /models index does),
   and neither does the Dashboard's own filter. */
.model-pill {
  display: inline-block;
  background: var(--accent-soft);
  color: var(--accent-dark);
  border-radius: 999px;
  padding: 3px 10px;
  font-size: 12px;
  font-weight: 600;
  text-decoration: none;
  margin: 2px 4px 2px 0;
}

.model-pill:hover {
  background: var(--accent);
  color: var(--accent-contrast);
}

.model-pill-approved {
  background: var(--model-gold-soft);
  color: var(--model-gold);
}

/* /models' own Trending section (public/models.js's loadTrending) — round
   46 request for the same gold/amber look .model-pill-approved gets, but
   under its own class rather than reusing that one directly: a trending
   pill isn't making any approval claim (loadTrending's own comment on why
   it deliberately skips the ★ prefix an approved pill also gets), so it
   shouldn't borrow a class whose name asserts that meaning — same color,
   different reason for having it. */
.model-pill-trending {
  background: var(--model-gold-soft);
  color: var(--model-gold);
}

/* .profile-pill — the /profiles directory's own Trending strip
   (public/profiles.js's loadTrending), same base pill shape as .tag-pill/
   .model-pill just above (own class rather than reusing either directly —
   this links to a person, not a taxonomy value, same "each kind of thing
   gets its own class even when visually identical" convention .model-pill's
   own comment already follows relative to .tag-pill). No approval/trending
   color variant needed the way Models has (.model-pill-approved/
   -trending) — a profile has no such states, just plain or nothing. */
.profile-pill {
  display: inline-block;
  background: var(--accent-soft);
  color: var(--accent-dark);
  border-radius: 999px;
  padding: 3px 10px;
  font-size: 12px;
  font-weight: 600;
  text-decoration: none;
  margin: 2px 4px 2px 0;
}

.profile-pill:hover {
  background: var(--accent);
  color: var(--accent-contrast);
}

/* .tag-banner also lives here (not browse.css) for the same reason as
   .tag-pill above — the dashboard's own tag filter (dashboard.js) reuses
   this exact "Filtered/Tagged #X ✕ clear" banner look, not just the public
   /tag/NAME and /bookmark/:id pages (browse.js / lib/publicPages.js). */
.tag-banner {
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--accent-soft);
  color: var(--accent-dark);
  border-radius: 8px;
  padding: 10px 14px;
  margin-bottom: 14px;
  font-size: 13.5px;
}

.tag-banner-clear {
  color: var(--accent-dark);
  font-weight: 600;
  text-decoration: none;
}

.tag-banner-clear:hover {
  text-decoration: underline;
}

/* Dashboard-only: each editable field (tags, notes) defaults to a compact
   read display (pills / plain text) with a small "Edit" toggle, rather than
   always showing the raw input/textarea the way this row used to. Clicking
   the toggle swaps that one field's display <-> input and flips the
   toggle's own label between "Edit"/"Done" — see dashboard.js's
   wireFieldToggle(). */
.row-field {
  margin-top: 6px;
}

.row-field-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.row-field-label {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  font-weight: 700;
}

.row-field-edit {
  background: none;
  border: none;
  color: var(--accent);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  padding: 0;
  font-family: inherit;
}

.row-field-edit:hover {
  text-decoration: underline;
}

.row-tags-display,
.row-models-display {
  margin-top: 3px;
  /* Matches .row-notes-display's own explicit font-size below. Tag/Model
     PILLS already set their own font-size (see .tag-pill/.model-pill in
     this file), so this only affects each one's "No tags"/"No models" empty
     state (plain .row-field-empty text, no pill) — without it, that text
     inherited a larger ambient size than "No notes" even though all three
     share the identical .row-field-empty span. */
  font-size: 12.5px;
}

.row-notes-display {
  margin: 3px 0 0;
  font-size: 12.5px;
  color: var(--text);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.row-field-empty {
  color: var(--text-muted);
  font-style: italic;
}

/* Featured-image picker (public/dashboard.js's renderImagePicker) — a grid
   of candidate thumbnails rather than a text display/input pair, so this
   doesn't reuse .row-tags-display/.row-notes-display, just the same
   .row-field/.row-field-header/.row-field-edit shell every other field
   already uses above it. */
.row-image-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 6px;
}

.row-image-picker-status {
  color: var(--text-muted);
  font-style: italic;
  font-size: 12.5px;
  margin: 4px 0 0;
}

.row-image-option {
  padding: 0;
  border: 2px solid transparent;
  border-radius: 8px;
  background: none;
  cursor: pointer;
  line-height: 0;
  flex: 0 0 auto;
}

.row-image-option img {
  width: 56px;
  height: 56px;
  object-fit: cover;
  border-radius: 6px;
  display: block;
}

.row-image-option:hover {
  border-color: var(--border);
}

/* The candidate currently set as this URL's featured image (urls.image_url)
   — same accent color every other "this one's active" state in this app
   uses (.vote-active, .admin-tab-active), so it reads consistently. */
.row-image-option-active {
  border-color: var(--accent);
}

.row-image-option:disabled {
  opacity: 0.6;
  cursor: default;
}

.row-visibility {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 6px;
  font-size: 12px;
  font-weight: 500;
  color: var(--text-muted);
  cursor: pointer;
  width: fit-content;
}

.row-visibility input[type="checkbox"] {
  width: auto;
  margin: 0;
}

.row-delete {
  background: none;
  border: none;
  color: #bbb;
  font-size: 14px;
  cursor: pointer;
  padding: 2px 6px;
  border-radius: 6px;
}

.row-delete:hover {
  color: #fff;
  background: #c62828;
}

/* "Set as collection cover" star (public/manageCollections.js's manageRowHtml,
   only offered on a row whose bookmark actually has an image) — outline
   star in the collection's normal unremarkable state, filled accent-colored
   star once it's this collection's current explicit cover_bookmark_id, same
   "outline vs. filled" convention #followBtn.following already uses
   elsewhere in this file for an active/toggled state. */
.row-cover-toggle {
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  padding: 2px 6px;
  border-radius: 6px;
  flex: 0 0 auto;
}

.row-cover-toggle:hover {
  color: var(--accent);
  background: var(--accent-soft);
}

.row-cover-toggle.active {
  color: var(--accent);
}

.empty {
  color: var(--text-muted);
  text-align: center;
  padding: 40px 0;
}

/* /partners — a plain link index, same multi-column layout as /tags' own
   .tag-index (browse.css) but kept here in the sitewide stylesheet instead,
   since partners.html doesn't load browse.css for anything else. No count
   badge (.tag-index-count's counterpart) — a partner link has nothing
   analogous to a tag's bookmark count to show. */
.partners-index {
  column-width: 220px;
  column-gap: 28px;
}

.partners-index-item {
  display: block;
  padding: 9px 0;
  border-bottom: 1px solid var(--border);
  text-decoration: none;
  color: var(--text);
  font-size: 14px;
  break-inside: avoid;
}

.partners-index-item:hover .partners-index-name {
  text-decoration: underline;
}

.partners-index-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  display: block;
}

/* Mask Content mode (app.js's toggleMaskMode) — same masked-text-plus-blur-
   overlay treatment browse.css's own .tag-index-name/.bm-sitename etc. use,
   just kept here since partners.html doesn't load browse.css for anything
   else (see .partners-index's own comment above). A partner's name is as
   identifying as a tag or site name once this app's content is mostly
   adult — someone glancing at a screen shouldn't be able to read off which
   partner sites this account trades traffic with any more than they can
   read off a bookmark's own tags/site name elsewhere. The href itself is
   untouched (same as every other masked class) — only the visible text is
   hidden, so the link still works normally for the person actually using
   the page. */
body.mask-mode .partners-index-name {
  position: relative;
  color: transparent !important;
}

body.mask-mode .partners-index-name::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--text-muted);
  border-radius: 4px;
  opacity: 0.55;
}

/* Quick Hide (public/app.js's initQuickHide) — a floating button plus a
   full-viewport cover, injected on every page that loads app.js when
   QUICK_HIDE_ENABLED is on. The z-index on both is a deliberately extreme
   value (the common "nuclear option" max-int, same one cookie-consent
   banners/paywalls use) rather than just "higher than frame.css's own
   highest, 1000" — an ad creative this app has no control over the markup
   of (see ads.js's own comments on treating embed code as opaque) could set
   its own arbitrarily high z-index, and Quick Hide covering the page is the
   one thing here that has to win against literally everything else on it,
   including that. */
.quick-hide-btn {
  position: fixed;
  bottom: 16px;
  right: 16px;
  z-index: 2147483647;
  padding: 8px 14px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-muted);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
  cursor: pointer;
  opacity: 0.85;
}

.quick-hide-btn:hover,
.quick-hide-btn:focus-visible {
  opacity: 1;
  color: var(--text);
  border-color: var(--accent);
}

/* Kept visually low-key on purpose — muted colors, no alarming red/icon —
   so it doesn't itself draw a passerby's eye or read as "this button hides
   something sensitive" at a glance; the button still needs to be easy to
   FIND on purpose (fixed corner, always present, a title tooltip on hover),
   just not easy to notice for anyone who isn't the visitor themselves. */

.quick-hide-overlay {
  position: fixed;
  inset: 0;
  z-index: 2147483647;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* The "Enjoying it?" trial-signup nudge (public/promoNudge.js) — a small
   toast anchored to the opposite bottom corner from Quick Hide's own
   floating button just above (bottom-left here, Quick Hide stays
   bottom-right) so the two can never overlap on a page that has both. A
   far lower z-index than Quick Hide's deliberately-maximal one — this has
   no reason to ever need to win against an ad creative's own stacking
   context the way Quick Hide's "cover literally everything" job does, 1000
   already matches this app's own highest ordinary z-index elsewhere
   (frame.css). */
.qb-promo-nudge {
  position: fixed;
  left: 16px;
  bottom: 16px;
  z-index: 1000;
  max-width: 320px;
  padding: 14px 32px 14px 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18);
}

.qb-promo-nudge-text {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text);
}

.qb-promo-nudge-brand {
  font-weight: 600;
}

.qb-promo-nudge-link {
  color: var(--accent);
  font-weight: 600;
  text-decoration: underline;
}

.qb-promo-nudge-close {
  position: absolute;
  top: 6px;
  right: 8px;
  border: none;
  background: none;
  color: var(--text-muted);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  padding: 4px;
}

.qb-promo-nudge-close:hover,
.qb-promo-nudge-close:focus-visible {
  color: var(--text);
}

.quick-hide-overlay[hidden] {
  display: none;
}

/* A plain spinner, nothing else — no text, no branding, nothing that reads
   as anything other than "this page is loading," which is both the most
   boring thing to glance at and, since Quick Hide's redirect really is in
   flight underneath it, not even inaccurate. */
.quick-hide-overlay-spinner {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 3px solid var(--border);
  border-top-color: var(--text-muted);
  animation: quick-hide-spin 0.8s linear infinite;
}

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

/* Mask Content mode (public/app.js's setMaskMode/hydrateMaskModeToggle) —
   Admins and Members only. Unlike Quick Hide just above, this ISN'T a
   floating always-there control: the icon button lives inline in the site
   nav, right before #userIdentity (built into #siteNav by
   buildMaskModeToggle), and only ever exists in the DOM at all once
   eligibility is confirmed. A plain icon button, not `.btn`'s usual
   padded/labeled pill (app.js swaps its whole innerHTML for
   PASSWORD_EYE_ICON/PASSWORD_EYE_OFF_ICON — the exact same eye-glyph
   markup wirePasswordToggle's own button already uses elsewhere on this
   site, same "this is hidden" visual language, not a second one invented
   just for this control) — sized/shaped like .nav-toggle just above (the
   hamburger button), the closest existing precedent for a small icon-only
   button sitting in this same header. */
.mask-mode-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-muted);
  cursor: pointer;
}

.mask-mode-toggle svg {
  width: 20px;
  height: 20px;
}

/* Both eye icons are embedded in this button's own markup from the very
   start (server-rendered, see MASK_MODE_TOGGLE_HTML in lib/brand.js) —
   CSS alone decides which one is visible, purely off the .active class
   just below, rather than public/app.js swapping one <svg>'s innerHTML for
   the other. That's what lets the CORRECT icon show up the instant this
   button is revealed — including before app.js has even loaded, by
   EARLY_MASK_TOGGLE_SCRIPT_HTML (lib/brand.js) — with no separate
   "now go pick the right icon" step required. .mmt-icon-off (the open
   eye — masking is OFF) is this button's default state below; .active
   swaps which one is shown, same "look at the class, not a separate
   variable" pattern browse.css's own .view-toggle-btn.active already
   uses for its own icon-swap buttons. */
.mask-mode-toggle .mmt-icon-on {
  display: none;
}

.mask-mode-toggle.active .mmt-icon-off {
  display: none;
}

.mask-mode-toggle.active .mmt-icon-on {
  display: inline;
}

.mask-mode-toggle:hover {
  border-color: var(--accent);
  color: var(--accent);
}

/* The "currently masked" state — filled in with --accent rather than left
   at the plain hover treatment above, so a visitor can tell at a glance
   that content IS masked right now without having to look closely at which
   of the two eye glyphs is showing. Same active-state color swap
   browse.css's own `.view-toggle-btn.active`/`.sort-btn.active` already use
   for "which of these is currently selected." */
.mask-mode-toggle.active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}

.mask-mode-toggle.active:hover {
  color: var(--accent-contrast);
}

/* ---- Topographical mode — admin-only spacing/layout debug overlay ----
   (public/app.js's isTopoModeOn/applyTopoModeClass/renderTopoOverlays).
   Turned on/off from a checkbox on Account settings (account.html's own
   #topoModePanel, admin-only, left in place) AND, again as of the round-32
   nav-order request, from a nav toggle button right next to the Mask icon
   — .topo-mode-toggle just below, the same 36px icon-button shape as
   .mask-mode-toggle above, briefly retired when the switch first moved to
   Account settings alone and un-retired here rather than replacing that
   panel (see TOPO_MODE_TOGGLE_HTML's own comment in lib/brand.js). Below
   that is the overlay layer itself: colored boxes for ad slots/images, a
   neutral tint for every other layout block, and a labeled px value in
   every real gap between them. Every overlay rule below is scoped under
   body.topo-mode so none of it does anything until an admin actually turns
   this on. */

/* Single icon (not two, unlike Mask mode's eye/eye-off swap) — there's no
   obvious "off" glyph for a debug overlay the way an open/closed eye reads
   for masking, so the .active class alone carries the on/off distinction,
   same background/border/color swap .mask-mode-toggle.active already uses
   just above. */
.topo-mode-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-muted);
  cursor: pointer;
}

.topo-mode-toggle svg {
  width: 20px;
  height: 20px;
}

.topo-mode-toggle:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.topo-mode-toggle.active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}

.topo-mode-toggle.active:hover {
  color: var(--accent-contrast);
}

/* The overlay layer itself — a single element appended as the LAST child
   of <body>, sized by its own absolutely-positioned children rather than
   any explicit height of its own. `position: absolute` (not `fixed`) on
   both this and every child box/label below is the whole trick that lets
   them scroll together with the real page content they're measuring,
   using plain document coordinates (rect.top/left + window.scrollX/Y —
   see renderTopoOverlays' own addBox/addLabel) instead of having to
   re-position anything on every scroll event. pointer-events: none keeps
   the whole layer from blocking clicks on the real page underneath it. */
#topoOverlayLayer {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  pointer-events: none;
  z-index: 999998;
}

.topo-box {
  position: absolute;
  box-sizing: border-box;
  pointer-events: none;
}

.topo-box-ad {
  background: rgba(245, 158, 11, 0.16);
  outline: 2px dashed rgba(217, 119, 6, 0.9);
  outline-offset: -2px;
}

/* A genuinely empty ad slot (no admin embed code configured — the common
   case in a dev/test environment) still gets floored to a visible 3px-tall
   marker by addBox above; the dashed-only, no-fill treatment here keeps
   that from reading as "a populated ad this short," which it isn't. */
.topo-box-ad-empty {
  background: none;
}

.topo-box-image {
  background: rgba(20, 184, 166, 0.14);
  outline: 2px solid rgba(13, 148, 136, 0.75);
  outline-offset: -2px;
}

.topo-box-neutral {
  background: rgba(100, 116, 139, 0.07);
  outline: 1px dashed rgba(100, 116, 139, 0.35);
  outline-offset: -1px;
}

.topo-label {
  position: absolute;
  pointer-events: auto;
  user-select: text;
  background: rgba(15, 23, 42, 0.92);
  color: #fff;
  font: 600 11px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  padding: 2px 6px;
  border-radius: 4px;
  white-space: nowrap;
  max-width: 90vw;
  overflow: hidden;
  text-overflow: ellipsis;
}

.topo-label-ad {
  background: rgba(180, 83, 9, 0.95);
}

/* Gap labels sit just inside the content column's own left edge (see
   renderTopoOverlays' own containerLeft), vertically centered in whatever
   real, post-collapse gap they're reporting on — amber when either
   neighbor on either side of that gap is an ad slot (the specific spacing
   this tool exists to clean up), plain neutral gray otherwise. */
.topo-label-gap {
  background: rgba(51, 65, 85, 0.92);
  font-weight: 700;
}

.topo-label-gap-ad {
  background: rgba(180, 83, 9, 0.95);
}

/* Bottom-LEFT, not bottom-right — Quick Hide's own always-present floating
   button (.quick-hide-btn above, when QUICK_HIDE_ENABLED) already lives in
   that corner at a deliberately maximal z-index, and this legend isn't
   worth contesting that corner with it. */
#topoLegend {
  position: fixed;
  left: 12px;
  bottom: 12px;
  z-index: 999999;
  background: rgba(15, 23, 42, 0.94);
  color: #fff;
  font: 500 11px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  padding: 8px 10px;
  border-radius: 8px;
  max-width: 260px;
  pointer-events: none;
}

#topoLegend div {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 3px;
}

#topoLegend div:first-child {
  margin-top: 0;
}

.topo-swatch {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 2px;
  flex: none;
}

.topo-swatch-ad {
  background: rgba(245, 158, 11, 0.9);
}

.topo-swatch-image {
  background: rgba(20, 184, 166, 0.85);
}

.topo-swatch-neutral {
  background: rgba(148, 163, 184, 0.6);
}

/* The masked-thumbnail placeholder — an inline SVG data URI, not a static
   image file (this app has no static image assets anywhere in public/ at
   all; every icon anywhere on the site is inline markup, favicon included —
   see lib/brand.js's own icon field, a plain emoji).
   Deliberately an eye-with-a-line-through-it glyph, the same "this is
   deliberately hidden" visual language app.js's own password-reveal icons
   (PASSWORD_EYE_OFF_ICON) already use elsewhere on this site — and
   deliberately NOT the picture-frame/dot/diagonal-line glyph
   .row-thumb-placeholder/.bm-image-placeholder already use for "this image
   failed to load" (see browse.js's thumbPlaceholderHtml): those two need to
   read as different situations to a visitor, not the same one twice. Colors
   are hardcoded (a data URI can't see this page's --accent/--text-muted
   custom properties — those only exist in the parent document, not inside
   an independently-fetched image resource) to a neutral gray that reads
   fine against either this app's light or dark palette, rather than
   matching either one exactly.
   Declared as a custom property (--mask-thumb-icon), not repeated inline
   wherever it's needed, since browse.css's own copy of this same rule (for
   .bm-favicon/.profile-collection-cover — see that file) needs the
   identical icon; a custom property set here on :root is visible there too,
   same as --accent and the rest of this file's own tokens already are
   across every stylesheet that loads after this one. */
:root {
  --mask-thumb-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 72'%3E%3Crect width='72' height='72' rx='8' fill='%23d7dbdb'/%3E%3Cg fill='none' stroke='%23666f6f' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 36s7-11 18-11 18 11 18 11-7 11-18 11S18 36 18 36Z'/%3E%3Ccircle cx='36' cy='36' r='5'/%3E%3Cpath d='M14 14l44 44'/%3E%3C/g%3E%3C/svg%3E");
}

/* .row-favicon (this file) masked the same way as the thumbnail above —
   small as it is, a favicon can still be identifying (a distinctive site
   logo), same reasoning as everything else in this block.
   `content: url(...)` reliably swaps a genuine replaced element's own
   rendering — an <img>, concretely — the same way in every current engine,
   which is what every selector below actually is.
   `.row-thumb` is the one exception worth calling out: in grid view, a
   bookmark with no captured image_url renders a plain <div>/<span>
   placeholder that ALSO carries the .row-thumb class (browse.js's
   thumbPlaceholderHtml — see .list.grid-view .row-delete's own comment
   below for why), so this selector matches both a real <img> and that
   placeholder div. An earlier version of this rule applied `content:url()`
   to BOTH — which briefly caused a real, reported bug: Chromium quietly
   ignores `content` on a non-replaced element like that div (`content` is
   only well-defined for genuinely replaced elements, an <img> among them),
   so nothing visibly broke there, but Firefox instead manufactures a
   brand-new replaced box sized off the swapped image's OWN intrinsic
   dimensions, ignoring the placeholder div's actual CSS width/height
   entirely — and a follow-up attempt to fix that by moving the swap onto a
   ::after overlay instead broke the OTHER, much more common case just as
   badly: ::before/::after generated content has no effect on a replaced
   element like <img> at all (also spec-defined, also true in every engine),
   so every REAL thumbnail across the whole site silently stopped masking.
   The actual fix is to keep `content: url(...)` for the real <img> case
   below (element-qualified as `img.row-thumb`, not bare `.row-thumb`, so
   this never touches the placeholder div) and handle that placeholder
   separately, with a technique that has no replaced-element ambiguity at
   all — see its own rule, right after this one. */
body.mask-mode img.row-thumb,
body.mask-mode img.bm-image,
body.mask-mode .row-favicon,
body.mask-mode .collection-card-cover,
/* Featured-image picker thumbnails (public/dashboard.js's
   renderImagePicker) — the same scraped page content as .row-thumb above,
   just an alternate candidate rather than the currently-chosen one, so
   this needs the same cover-it-up treatment for the same shoulder-surfing
   reason. Only ever visible while a row's picker panel is actively open
   (row-image-picker starts [hidden]), but mask mode shouldn't leave a gap
   for the one deliberate interaction that opens it. */
body.mask-mode .row-image-option img {
  content: var(--mask-thumb-icon);
}

/* .bm-image on its own, right after the shared rule above — reported: with
   only that rule in place, masking an image bookmark's big permalink-page
   thumbnail didn't just swap in the icon, it blew it up to fill most of the
   viewport. Same underlying class of `content: url()` replaced-element
   sizing bug as the Firefox one already documented above (search this
   file for ".row-thumb-placeholder already use for"), just tripped a
   different way: `img.row-thumb`/`.row-favicon`/`.collection-card-cover`/
   `.row-image-option img` all give their <img> a DEFINITE pixel width AND
   height (see browse.css/this file's own rules for each), so swapping their
   content never has anywhere ambiguous to size itself into — `object-fit:
   cover` just crops the new (also square) icon to that fixed box, the same
   way it already crops a real photo. `.bm-image` (browse.css) is the one
   exception: `width: 100%; max-height: 320px;` — a PERCENTAGE width and
   only a max-height CAP, never a definite height — which is fine for a real
   photo (browsers resolve its own intrinsic ratio into a sane auto height
   that max-height only occasionally even needs to clamp) but left the
   swapped-in icon's own box height under-constrained enough that at least
   one engine rendered it far larger than 320px instead of clamping down to
   it. Giving `.bm-image` the same "definite pixel box, no ambiguity left
   for any engine to resolve differently" treatment as its four siblings —
   just scoped to mask mode, so the normal (unmasked) photo rendering, which
   was never actually broken, keeps its existing percentage-width/max-height
   sizing untouched. */
body.mask-mode .bm-image {
  height: 320px;
}

/* Round 49: reported again — a bookmark with NO captured image (or one
   whose image_url 404s client-side, lib/publicPages.js's onerror fallback)
   renders `.bm-image-placeholder`, a plain <span> that shares the bare
   `.bm-image` class above but isn't an <img> at all. The shared content-swap
   rule further up used to list that bare class (not `img.bm-image`), so it
   swapped this placeholder's `content` too — exactly the same replaced-
   element ambiguity `.row-thumb`/`.row-thumb-placeholder` were already split
   apart to avoid (see that rule's own comment), just never applied
   symmetrically to this element's own placeholder twin: the height:320px
   above still resolves correctly (it targets the plain class, so it wins on
   specificity regardless of which element it's on), but with no
   `overflow:hidden` here, an oversized replaced box from the ambiguous swap
   just spills out of this flex container instead of being clamped to it —
   reported as the mask icon blowing up over the surrounding page. Fixed the
   same way as .row-thumb/.row-thumb-placeholder: the shared rule above is
   now element-qualified as `img.bm-image` (real <img> only, no ambiguity),
   and this placeholder gets its own background-image swap instead — see
   .row-thumb-placeholder's own rule just below for the identical
   technique/reasoning, copied verbatim. */
body.mask-mode .bm-image-placeholder {
  background-color: #d7dbdb;
  background-image: var(--mask-thumb-icon);
  background-repeat: no-repeat;
  background-size: 100% 100%;
}

body.mask-mode .bm-image-placeholder svg {
  visibility: hidden;
}

/* The no-captured-image placeholder (.row-thumb-placeholder — see the
   comment just above for why it needs its own rule, separate from the real
   <img> case). A plain <div>/<span> can take a background-image directly
   with zero replaced-element ambiguity in any engine, so that's what this
   uses instead of `content` — same "cover it with the mask icon" result,
   just via a technique that can't silently do the wrong thing depending on
   the browser. The placeholder's own inline "no image" SVG glyph
   (.row-thumb-placeholder svg, right below) is hidden underneath rather
   than removed, so nothing else about this element has to change; the mask
   icon's own opaque background rect (baked into the --mask-thumb-icon data
   URI — see its own comment above) fully covers the placeholder's normal
   --accent-soft background either way. */
body.mask-mode .row-thumb-placeholder {
  background-color: #d7dbdb;
  background-image: var(--mask-thumb-icon);
  background-repeat: no-repeat;
  background-size: 100% 100%;
}

body.mask-mode .row-thumb-placeholder svg {
  visibility: hidden;
}

/* Round 50: reported — some Mask Mode thumbnail placeholders on the
   Dashboard "span the rectangle" (the eye-slash icon, correctly stretched
   to fill) while others show as a small square sitting inside the
   rectangle. The second case wasn't actually a masked icon at all: these
   two sibling "no cover image" placeholders never got the same treatment
   as .row-thumb-placeholder/.bm-image-placeholder above, so in Mask Mode
   they were still showing their own normal, UNMASKED "no image" glyph —
   which naturally renders small and centered, since nothing was stretching
   it to fill the box.
   - .collection-tile-thumb-placeholder (browse.css) — the Dashboard's own
     "Your collections" grid, and the Bookmark permalink page's "Public
     collections" section, which reuses the identical tile design.
   - .collection-card-cover-placeholder (browse.css) — the Manage
     Collections page's own cover thumbnail.
   Same technique as .row-thumb-placeholder just above, copied verbatim. */
body.mask-mode .collection-tile-thumb-placeholder,
body.mask-mode .collection-card-cover-placeholder {
  background-color: #d7dbdb;
  background-image: var(--mask-thumb-icon);
  background-repeat: no-repeat;
  background-size: 100% 100%;
}

body.mask-mode .collection-tile-thumb-placeholder svg,
body.mask-mode .collection-card-cover-placeholder svg {
  visibility: hidden;
}

/* Bookmark text — titles, descriptions, site-name pills, tags, the raw URL
   itself — covered by a solid bar rather than swapped for dots or blurred:
   a solid background-colored ::after positioned over the element's own box,
   so this never has to know how any given element's text was actually built
   (a plain text node here, an <a> there, a <span> pill elsewhere) — it
   works the same way regardless, by covering the box the text sits in
   rather than touching the text itself. `color: transparent` (not
   `visibility:hidden` or `opacity:0`) is what's doing the actual hiding —
   kept so the element keeps its normal layout size/line-wrapping exactly as
   before, which the overlay bar below then needs to still line up with.
   `!important` is needed for `.row-sitename`/`.tag-pill`'s own `color`
   declarations elsewhere in this file, which would otherwise win by sitting
   later in source order. .row-url is included here (not grouped with the
   thumbnail/favicon swap above) since it's plain text, not an image — the
   bookmark's actual target URL is at least as identifying as its title, so
   it needs the same bar rather than being left legible. .model-pill (a
   Model credit — lib/models.js's own comment: "a curated identity, e.g. a
   performer's name") joined this list for the same reason .tag-pill is
   here: a real person's name is at least as identifying as a tag, and
   mask mode had been covering every other piece of identifying text on a
   row except this one. Covers `.model-pill-approved`'s own "★ " prefix
   too, same box either way — no separate rule needed for that variant.
   `.profile-pill` (the /profiles directory's own Trending-by-followers
   strip) joined this list for the exact same reason `.model-pill` did — a
   username in that strip is at least as identifying as a tag, and this
   was the one piece of text on /profiles mask mode had missed (the A-Z
   listing's own `.tag-index-name` was already covered separately). */
body.mask-mode .row-title,
body.mask-mode .row-description,
body.mask-mode .row-sitename,
body.mask-mode .row-url,
body.mask-mode .tag-pill,
body.mask-mode .model-pill,
body.mask-mode .profile-pill {
  position: relative;
  color: transparent !important;
}

body.mask-mode .row-title::after,
body.mask-mode .row-description::after,
body.mask-mode .row-sitename::after,
body.mask-mode .row-url::after,
body.mask-mode .tag-pill::after,
body.mask-mode .model-pill::after,
body.mask-mode .profile-pill::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--text-muted);
  border-radius: 4px;
  opacity: 0.55;
}

/* Collections dashboard, /collections/manage (public/manageCollections.js)
   — a Collection's own name,
   same bar-overlay treatment as a bookmark's .row-title/.bm-title above and
   browse.css's own .profile-collection-name (the public-facing version of
   this same card), since a Collection's name can be just as identifying as
   a bookmark's own title. .collection-card-description is left unmasked on
   purpose — same "chrome, not necessarily identifying" call
   .profile-collection-description's own comment makes, and an owner
   managing their own private dashboard already sees their card's bookmark
   count and Public/Private badge in the clear either way. */
body.mask-mode .collection-card-title h2 {
  position: relative;
  color: transparent !important;
}

body.mask-mode .collection-card-title h2::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--text-muted);
  border-radius: 4px;
  opacity: 0.55;
}

/* Manage Collections page, /collections/manage
   (public/manageCollections.html/.js). One .panel (existing
   class, above) per collection, in a plain vertical stack — .collections-
   list, not .list, since these cards are much taller/denser than a bookmark
   .row and have no grid-view equivalent (unlike /browse and /dashboard,
   this page never offers a grid layout toggle). */
.collections-list {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.collection-card-header {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 4px;
}

/* Cover thumbnail (public/manageCollections.js's collectionCoverHtml) — the
   owner's explicit pick (setCollectionCover) or, absent one, the automatic
   "first member bookmark with an image" fallback both listMyCollections and
   getPublicCollectionsForUser compute server-side. Same 1px border/8px
   radius language .row-thumb already uses, just a little smaller since this
   sits inline in a card header rather than leading a list row. The
   placeholder variant (no image at all — an empty collection, or one whose
   every member is still imageless) reuses .row-thumb-placeholder's own
   icon/background treatment (browse.css) rather than inventing a second
   one, just sized to match this slot instead of a full .row-thumb. */
.collection-card-cover {
  width: 56px;
  height: 56px;
  object-fit: cover;
  border-radius: 8px;
  border: 1px solid var(--border);
  flex: 0 0 auto;
}

/* Wraps .collection-card-cover (the <img> or its placeholder <div>, only
   ever one visible at a time — see collectionCoverHtml's own comment) so
   collection-card-promoted-badge below has a relatively-positioned box to
   pin itself to, the way .collection-tile-thumb already doubles as its own
   box for the large-tile Promoted badge everywhere else. Sized to hug the
   cover exactly (56px, matching .collection-card-cover itself) and kept
   flex:0 0 auto so wrapping it changes nothing about .collection-card-header's
   own flex layout — this is a transparent wrapper, not a new visible
   element. */
.collection-card-cover-wrap {
  position: relative;
  width: 56px;
  height: 56px;
  flex: 0 0 auto;
}

/* Manage Collections' own scaled-down Promoted pill — see browse.css's
   .collection-tile-promoted-badge for the full-size version every other
   Collection surface uses and the shared reasoning/wording behind it. This
   page's own cover is a fixed 56x56px, far smaller than the large,
   flexible-width 16:9 tile every other context renders, so the full-size
   pill (11.5px font, 3px/7px padding) would badly overflow this one
   corner — scaled down roughly proportionally and floored at a still-
   legible size rather than shrunk to true scale, which would be
   unreadable. */
.collection-card-promoted-badge {
  position: absolute;
  left: 2px;
  top: 2px;
  font-size: 8px;
  font-weight: 600;
  line-height: 1;
  padding: 1px 3px;
  border-radius: 999px;
  color: #fff;
  background: rgba(180, 83, 9, 0.9);
  pointer-events: none;
}

.collection-card-cover-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-soft);
  color: var(--text-muted);
}

.collection-card-cover-placeholder[hidden] {
  display: none;
}

.collection-card-cover-placeholder svg {
  width: 40%;
  height: 40%;
  max-width: 24px;
  max-height: 24px;
}

.collection-card-title {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.collection-card-title h2 {
  margin: 0;
}

.collection-card-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  /* Pushed to the far right now that .collection-card-header has a third
     child (the cover thumbnail) ahead of the title — .collection-card-header
     no longer uses justify-content: space-between for this, since with three
     flex children that would spread the cover away from the title instead of
     keeping them together. */
  margin-left: auto;
}

/* Same pill shape as .row-sitename (browse.css) — reused, not redefined —
   just with the two semantic colors this app already uses elsewhere for
   "this is out in the open" vs. "this is just for you": accent-green for
   public (.verified-badge-yes / .auth-success's own green) and a plain
   muted tone for private, since private is this page's default/unremarkable
   state and doesn't need to read as a warning the way .row-dead-link's red
   does. */
.collection-badge {
  font-size: 11.5px;
  font-weight: 600;
  padding: 2px 9px;
  border-radius: 999px;
  flex: 0 0 auto;
}

.collection-badge-public {
  color: #2e7d32;
  background: rgba(46, 125, 50, 0.12);
}

.collection-badge-private {
  color: var(--text-muted);
  background: var(--accent-soft);
}

.collection-card-count {
  margin-bottom: 6px;
}

/* Same muted, small-print treatment as .row-views' own 👁 count elsewhere
   (dashboard.js/browse.js rows) — a secondary stat riding along after the
   count's own " · " separator, not competing with it for attention. Now
   two of these sit side by side (bookmark views, then the collection's own
   page views) — same class, same look, for both. */
.collection-card-views,
.collection-card-page-views {
  color: var(--text-muted);
  font-size: 0.9em;
}

.collection-card-description {
  margin-bottom: 10px;
}

.collection-public-link {
  display: inline-block;
  font-size: 13px;
  color: var(--accent);
  text-decoration: none;
  margin-bottom: 10px;
}

.collection-public-link:hover {
  text-decoration: underline;
}

.collection-edit-form {
  border-top: 1px solid var(--border);
  padding-top: 14px;
  margin-top: 4px;
}

.collection-manage {
  border-top: 1px solid var(--border);
  padding-top: 14px;
  margin-top: 4px;
}

.manage-count {
  margin-bottom: 10px;
}

/* One collection-manage-row per bookmark — .row (styles.css) plus a drag
   handle prepended and .row-delete (existing "✕" button) appended, same
   markup shape /dashboard's own rows already use for their own trailing
   delete button. */
.collection-manage-row {
  cursor: default;
}

.drag-handle {
  cursor: grab;
  color: var(--text-muted);
  font-size: 16px;
  line-height: 1;
  padding: 2px 4px;
  flex: 0 0 auto;
  user-select: none;
}

.drag-handle:active {
  cursor: grabbing;
}

/* Applied to the one row actually being dragged (public/manageCollections.js's
   wireDragAndDrop) — dimmed and slightly scaled down so it reads as "lifted
   out of the list" while the other rows visually reflow around it. */
.collection-manage-row.dragging {
  opacity: 0.4;
}

/* Admin page — Featured Collections tab, the "Currently featured" list.
   Same .row/.drag-handle/.dragging shapes as .collection-manage-row above
   (public/manageCollections.js), just for collections instead of a collection's
   own bookmarks — see public/admin.js's featuredCurrentRowHtml/
   wireFeaturedDragAndDrop. */
.featured-collection-row {
  cursor: default;
}

.featured-collection-row.dragging {
  opacity: 0.4;
}

/* Flags a currently-featured collection that's gone private since being
   featured (public/admin.js's featuredCurrentRowHtml) — still listed here
   so an admin can un-feature it or wait for it to go public again, but
   it's silently absent from the Browse page's own featured section in the
   meantime (see lib/collections.js's getFeaturedCollections, which filters
   on is_public unlike this admin listing). Amber rather than the report
   card's red — this is a "heads up," not an error. */
.featured-collection-warning {
  display: inline-block;
  font-size: 11px;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 100px;
  background: #fff3e0;
  color: #e65100;
  flex: 0 0 auto;
}

.collection-add-bookmark {
  margin-top: 14px;
}

.add-bookmark-search {
  width: 100%;
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 14px;
  font-family: inherit;
  margin-bottom: 8px;
}

.add-bookmark-search:focus {
  outline: none;
  border-color: var(--accent);
}

.collection-add-results {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.collection-add-result-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
}

.collection-add-result-row .row-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
