/*
 * Are.na Link Reader
 * Tokens pulled from aredotna/ervell src/v2/styles
 */

:root {
  /* Render native scrollbars, form controls, etc. in the OS theme so they
     match the prefers-color-scheme palette below (otherwise the UA paints a
     light scrollbar over the dark page). */
  color-scheme: light dark;
  --bg: #fff;
  --fg: #333;
  --fg-secondary: #585858;
  --fg-muted: #999;
  /* Rules and dividers (ervell gray.light). Control and container edges use
     --border-control (gray.regular) — a hairline border, never a shadow, is
     what separates a box from the page in Are.na. --border-active is the
     darker edge a control takes while it's open (gray.medium). */
  --border: #eee;
  --border-control: #ccc;
  --border-active: #999;
  --surface: #f7f7f7;
  --input-bg: rgb(247, 245, 244);
  --accent: #333;
  --link: #333;
  --success: #22c55e;
  /* ervell has exactly one radius (constants.borderRadius = 0.25em). There is
     no large/soft variant — resist adding one. */
  --radius: 0.25rem;
  --block-w: 315px;

  /* Spacing scale — 4px base. The single source of truth for gaps, padding,
     and margins; reach for these instead of one-off px/em values. 16px maps to
     the Figma "Semantics" item-padding-v token, 8px to item-gap. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;
  --gutter: 14px;        /* feed column/row gutter (Are.na block rhythm) */

  /* Type scale — rem so it tracks the root size (18px mobile, 20px desktop).
     --text-ui (12px ref) is the default for controls and meta text. */
  --text-title: 0.875rem;   /* titles, modal headings */
  --text-body: 0.8125rem;   /* descriptions, secondary body text */
  --text-ui: 0.75rem;       /* default UI / control / meta text */
  --text-sm: 0.6875rem;     /* fine print, compact buttons */
  --text-label: 0.625rem;   /* uppercase micro-labels */
  --text-micro: 0.5625rem;  /* badges, avatar initials */
  --leading: 1.3;           /* body/multi-line text; single-line uses 1 */

  --control-h: calc(var(--text-ui) * 1.8125 + 2px);
  --maxw: 720px;
  --maxw-grid: 1000px;
  /* No elevation tokens by design: Are.na has no drop shadows anywhere. Depth
     is a 1px border, and that is the whole vocabulary — a floating panel gets
     the same hairline edge as a docked one, nothing more. */
  /* Skeleton placeholder fill + the lighter band the shimmer sweeps across */
  --skeleton-base: #e9e9e9;
  --skeleton-sheen: #f4f4f4;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #000;
    --fg: #d3d3d3;
    --fg-secondary: #d3d3d3;
    --fg-muted: #6e6e6e;
    --border: #2f2f2f;
    --border-control: #444;
    --border-active: #6e6e6e;
    --surface: #131313;
    --input-bg: #080a0b;
    --accent: #d3d3d3;
    --link: #d3d3d3;
    --skeleton-base: #1d1d1d;
    --skeleton-sheen: #2a2a2a;
  }
}

* { box-sizing: border-box; margin: 0; padding: 0; }

html, body {
  background: var(--bg);
  color: var(--fg);
  font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;
  font-size: 20px;
  line-height: var(--leading);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

@media (max-width: 640px) {
  /* Lift the mobile root size closer to desktop (20px) so the rem-based
     feed text — title, description, source, attribution, meta — reads
     comfortably on small screens. */
  html { font-size: 18px; }
  body { overflow-x: hidden; }
}

a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; }

code {
  font-family: Menlo, Monaco, 'Bitstream Vera Sans Mono', Consolas, Courier, monospace;
  font-size: 0.8125em;
}

/* Honor reduced-motion globally: collapse every transition to instant. Little
   is left to collapse — menus, presses, and badges no longer animate at all —
   but it still covers the modal/toast fades, the item exit, and the scroll-
   driven nav bars. Keyframe loaders (skeleton, spinner) keep gentle,
   non-vestibular motion via their own reduced-motion rules below. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---- topbar ---- */
.topbar {
  position: sticky;
  top: 0;
  z-index: 10;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
  height: auto;
  padding-top: env(safe-area-inset-top, 0);
  transition-property: transform, box-shadow;
  transition-duration: 200ms;
  transition-timing-function: cubic-bezier(0.2, 0, 0, 1);
}
.topbar.nav-hidden {
  transform: translateY(-100%);
  transition-duration: 150ms;
  transition-timing-function: ease-in;
}
.topbar-inner {
  padding: var(--space-3) max(1em, env(safe-area-inset-right, 0)) var(--space-3) max(1em, env(safe-area-inset-left, 0));
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  position: relative;
}
.topbar h1 {
  font-size: var(--text-title);
  font-weight: 600;
  letter-spacing: 0;
  white-space: nowrap;
  flex-shrink: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
  user-select: none;
}
.topbar h1:focus-visible {
  outline: 2px solid var(--fg-muted);
  outline-offset: 2px;
}

.controls {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--space-2);
  align-items: center;
  flex: 1;
  min-width: 0;
}
.controls[hidden] { display: none; }
/* On desktop: overlay that spans the full topbar width so the buttons sit at
   the true viewport midpoint. pointer-events:none lets clicks through to the
   h1/icon buttons on either side; the .filter-select children restore them.
   Centering via inset:0 + justify-content avoids transform, which breaks CSS
   anchor positioning in Safari (anchor coords are pre-transform in that engine).
   On mobile it becomes a fixed bottom bar (see media query below). */
.select-row {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  pointer-events: none;
}
.select-row .filter-select {
  pointer-events: auto;
}
/* Equal-width buttons on desktop: JS sets --desktop-btn-w to the widest option
   label across all three dropdowns so they all match without hardcoding sizes.
   Scoped to desktop-only so the variable doesn't bleed into the mobile bottom
   bar after an orientation change (portrait third ≈ 114px < landscape ~120px). */
@media (min-width: 641px) {
  .select-row .filter-select-btn {
    min-width: var(--desktop-btn-w, auto);
    /* Match the fixed-height icon buttons (see .controls .icon-btn): pin the
       trigger to --control-h and drop the vertical padding it would otherwise
       add, so the dropdowns line up with the icons instead of running taller.
       Content stays centered via align-items:center above. */
    height: var(--control-h);
    padding-top: 0;
    padding-bottom: 0;
  }
}
.filter-select-btn,
.controls > button,
.controls > a {
  font: inherit;
  font-size: var(--text-ui);
  line-height: var(--leading);
  color: var(--fg-secondary);
  background: var(--bg);
  border: 1px solid var(--border-control);
  border-radius: var(--radius);
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
}
/* Custom scope/sort/type dropdown trigger — looks like the old <select> box,
   with the chevron now a real flex item so it can flip on open. */
.filter-select { display: inline-flex; min-width: 0; }
.filter-select-btn {
  display: inline-flex;
  align-items: center;
  /* Label left, chevron pinned right (gap is the minimum spacing when the
     button is content-sized on desktop). */
  justify-content: space-between;
  gap: var(--space-2);
  min-width: 0;
  /* Chevron sits inside as a flex item, so trim the right padding it used to
     reserve as a background gutter. */
  padding-right: var(--space-2);
}
.filter-select-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.filter-select-chevron {
  flex-shrink: 0;
  display: inline-flex;
  color: var(--fg-muted);
}
.filter-select-chevron span { display: inline-flex; }
/* Small chevron sized like the old select's background glyph. */
.filter-select-chevron svg { width: 0.875em; height: 0.875em; }
/* chevron-down closed, chevron-up while the menu is open (the trigger's
   aria-expanded is kept in sync with the popover's open state in app.js). */
.filter-select-btn .chev-up { display: none; }
.filter-select-btn[aria-expanded="true"] .chev-down { display: none; }
.filter-select-btn[aria-expanded="true"] .chev-up { display: inline-flex; }
.filter-select-btn:hover,
.controls > button:hover,
.controls > a:hover {
  border-color: var(--border-active);
}
/* Expanded: darken the edge and square the corners the options panel joins on
   (ervell Pulldown, `mode === 'expanded'`). This border change *is* the open
   affordance — the field visibly grows into the panel. */
.filter-select-btn[aria-expanded="true"] {
  border-color: var(--border-active);
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}
.controls .ghost {
  color: var(--fg-muted);
  border-color: transparent;
  background: transparent;
}
.controls .ghost:hover {
  border-color: var(--border);
}
.controls .spacer { flex: 1; }

/* view toggle — single button that shows the icon for the current layout */
#view-toggle .view-icon-list,
#view-toggle .view-icon-grid { display: none; }
#view-toggle[data-current="list"] .view-icon-list { display: block; }
#view-toggle[data-current="grid"] .view-icon-grid { display: block; }

/* ---- layout ---- */
.container {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 1em max(1em, env(safe-area-inset-right, 0)) calc(1em + env(safe-area-inset-bottom, 0)) max(1em, env(safe-area-inset-left, 0));
  transition-property: max-width;
  transition-duration: 150ms;
  transition-timing-function: ease-out;
}
.container.wide { max-width: none; }

/* ---- auth modal ---- */
/* Lock page scroll while the modal is open so the feed/skeleton behind the
   overlay can't scroll independently. --scrollbar-comp (set in showAuth right
   before locking) backfills the width the vanishing scrollbar gave up, so the
   layout doesn't shift sideways; it's 0px with overlay scrollbars. */
:root.modal-open {
  overflow: hidden;
  padding-right: var(--scrollbar-comp, 0px);
}
/* overlay + modal: animated open/close
   Base = closed/exit state (shorter, ease-in).
   .open = enter state (longer, spring-like curve). */
.auth-overlay,
.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: rgba(0, 0, 0, 0.25);
  opacity: 0;
  visibility: hidden;
  transition-property: opacity, visibility;
  transition-duration: 150ms;
  transition-timing-function: ease-in;
}
.auth-overlay.open,
.modal-overlay.open {
  opacity: 1;
  visibility: visible;
  transition-duration: 200ms;
  transition-timing-function: ease-out;
}
@media (prefers-color-scheme: dark) {
  .auth-overlay,
  .modal-overlay { background: rgba(0, 0, 0, 0.5); }
}
.auth,
.shortcuts-modal {
  position: fixed;
  z-index: 51;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: calc(100% - 2em);
  max-width: 400px;
  border: 1px solid var(--border-control);
  border-radius: var(--radius);
  padding: var(--space-8);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  background: var(--bg);
  /* The overlay behind it carries the transition; the dialog itself just
     appears. No slide, no blur — matching ervell's Modal, which has neither. */
  opacity: 0;
  visibility: hidden;
  transition-property: opacity, visibility;
  transition-duration: 150ms;
  transition-timing-function: ease-in;
}
.auth.open,
.shortcuts-modal.open {
  opacity: 1;
  visibility: visible;
  transition-duration: 200ms;
  transition-timing-function: ease-out;
}
/* ---- keyboard shortcut sheet ---- */
/* Cap height to the viewport (1em breathing room top/bottom) so the centered
   box never grows taller than the screen and clips off the top edge on short
   breakpoints; the list scrolls internally while the header/close stay pinned.
   svh (smallest viewport height) keeps it fitting even while mobile browser
   chrome is visible; vh is the fallback for browsers without svh support. */
.shortcuts-modal {
  max-height: calc(100vh - 2em);
  max-height: calc(100svh - 2em);
}
.shortcuts-head strong { font-size: var(--text-title); }
.shortcuts-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  overflow-y: auto;
  min-height: 0;
}
.shortcut-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.shortcut-group-label {
  font-size: var(--text-label);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--fg-muted);
}
.shortcut-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
}
.shortcut-label {
  font-size: var(--text-body);
  color: var(--fg-secondary);
}
.shortcut-keys {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  flex-shrink: 0;
}
.shortcut-key-sep {
  font-size: var(--text-sm);
  color: var(--fg-muted);
}
.shortcuts-list kbd {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 22px;
  padding: 0 var(--space-2);
  font-family: inherit;
  font-size: var(--text-ui);
  line-height: 1;
  color: var(--fg);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.auth-close {
  position: absolute;
  top: 16px;
  right: 16px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--control-h);
  height: var(--control-h);
  padding: 0;
  border: 1px solid var(--border-control);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--fg-muted);
  cursor: pointer;
}
.auth-close[hidden] { display: none; }
.auth-close:hover { color: var(--fg); border-color: var(--border-active); }
.auth-user {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.auth-user[hidden] { display: none; }
/* Same .avatar component as feed attribution; img/fallback swap via hidden */
.auth-user .avatar[hidden] { display: none; }
.auth-username {
  font-size: var(--text-body);
  color: var(--fg-secondary);
}
.auth-check { color: var(--success); }
.auth-x { color: #ef4444; }
.auth-brand {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.auth-brand strong { font-size: var(--text-title); }
.auth-brand span {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-body);
  color: var(--fg-secondary);
}
.auth-brand span svg { color: var(--success); flex-shrink: 0; }
/* Signed in: the tagline becomes the "account is connected" status row */
.auth-brand .auth-user:not([hidden]) + span { color: var(--fg); }
.auth p {
  font-size: var(--text-sm);
  line-height: var(--leading);
  color: var(--fg-muted);
  margin-bottom: 0;
}
.auth p a { text-decoration: underline; }
.oauth-connect {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.oauth-connect[hidden] { display: none; }
.auth-btn.oauth-primary {
  flex: 1;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-ui);
  border-color: var(--accent);
  background: var(--accent);
  color: var(--bg);
}
.auth-btn.oauth-primary:hover {
  border-color: var(--accent);
  color: var(--bg);
  opacity: 0.9;
}
.remember-label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--fg-muted);
  cursor: pointer;
}
.remember-label[hidden] { display: none; }
.remember-label input {
  margin: 0;
  cursor: pointer;
}
.remember-label .auth-note { display: inline; margin-top: 0; }
.auth-note {
  display: block;
  margin-top: var(--space-2);
  font-size: var(--text-sm);
  color: var(--fg-muted);
}
.auth-note[hidden] { display: none; }
.auth-note code {
  font-family: inherit;
  font-size: inherit;
}
.auth-actions {
  display: flex;
  gap: var(--space-2);
}
.auth-actions .auth-btn {
  flex: 1;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-ui);
  color: var(--fg-secondary);
}
.auth-actions[hidden] { display: none; }
.auth-btn {
  font-family: inherit;
  font-size: var(--text-ui);
  line-height: var(--leading);
  color: var(--fg-muted);
  background: none;
  border: 1px solid var(--border-control);
  border-radius: var(--radius);
  padding: var(--space-1) var(--space-3);
  cursor: pointer;
  text-decoration: none;
}
.auth-btn:hover {
  border-color: var(--border-active);
  color: var(--fg);
  text-decoration: none;
}
.auth-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
  /* On very narrow modals, stack the two links onto their own lines instead of
     letting their text labels wrap mid-phrase. The labels are kept on one line
     (white-space: nowrap below), so the break happens at the item level only
     once they no longer fit side by side. */
  flex-wrap: wrap;
}
.auth-footer .remember-label,
.auth-footer .auth-source { white-space: nowrap; }
.auth-source {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-ui);
  color: var(--fg-muted);
}
.auth-source[hidden] { display: none; }
.auth-source:hover { color: var(--fg); text-decoration: none; }
.auth-source svg { width: 14px; height: 14px; flex-shrink: 0; }

/* ---- status ---- */
.status {
  border: 1px solid var(--border);
  padding: var(--space-3) var(--space-4);
  margin-bottom: 1em;
  font-size: var(--text-body);
}
.status.error {
  border-color: #b60202;
  color: #b60202;
}
@media (prefers-color-scheme: dark) {
  .status.error { border-color: #e24937; color: #e24937; }
}
.status.loading { color: var(--fg-muted); }

/* ---- skeleton loading ---- */
/* One placeholder primitive with two states: flat gray (a static preview of
   the feed layout, shown behind the locked sign-in modal) and shimmering
   (.skeleton--loading, an active "content is loading" signal during first
   load and media-type/filter transitions). Mirrors the feed's list and grid
   layouts so the swap to real items is seamless. */
.skeleton {
  display: flex;
  flex-direction: column;
}

.sk-item {
  display: grid;
  grid-template-columns: 1fr auto;
  column-gap: var(--gutter);
  padding: var(--space-4) 0;
  border-bottom: 1px solid var(--border);
}
.sk-item:first-child { padding-top: 0; }
.sk-item:last-child { border-bottom: 0; }

.sk-body {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.sk-foot {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding-top: var(--space-8);
}

.sk-line, .sk-thumb, .sk-dot {
  background: var(--skeleton-base);
  border-radius: var(--radius);
}
.sk-line { height: 0.7em; width: 100%; }
.sk-line-title { height: 0.9em; width: 72%; }
.sk-line-source { width: 35%; }
.sk-line-desc-end { width: 45%; }
.sk-line-meta { width: 30%; }
.sk-thumb {
  width: 100px;
  height: 100px;
  border-radius: 0;
}
.sk-dot {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  flex-shrink: 0;
}

@media (max-width: 640px) {
  .sk-item { grid-template-columns: 1fr 60px; }
  .sk-thumb { width: 60px; height: 60px; }
}

/* grid view — match .feed.grid cards (thumb on top, footer pinned bottom) */
.skeleton.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--block-w), 1fr));
  gap: var(--gutter);
  justify-content: center;
}
.skeleton.grid .sk-item {
  display: flex;
  flex-direction: column;
  padding: 0;
  border: 1px solid var(--border);
  overflow: hidden;
}
.skeleton.grid .sk-thumb {
  order: -1;
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;
  border-bottom: 1px solid var(--border);
}
.skeleton.grid .sk-body { padding: 0.5rem 0.5rem 0; }
.skeleton.grid .sk-foot {
  margin-top: auto;
  padding: var(--space-8) var(--space-3) var(--space-3);
}
@media (max-width: 640px) {
  .skeleton.grid { grid-template-columns: 1fr; gap: var(--space-4); }
}

/* shimmer — a lighter band sweeping across the base fill */
.skeleton--loading .sk-line,
.skeleton--loading .sk-thumb,
.skeleton--loading .sk-dot {
  background-image: linear-gradient(
    90deg,
    var(--skeleton-base) 25%,
    var(--skeleton-sheen) 50%,
    var(--skeleton-base) 75%
  );
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.4s ease-in-out infinite;
}
@keyframes skeleton-shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .skeleton--loading .sk-line,
  .skeleton--loading .sk-thumb,
  .skeleton--loading .sk-dot {
    background-image: none;
    animation: skeleton-pulse 1.6s ease-in-out infinite;
  }
  @keyframes skeleton-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.5; }
  }
}
/* Last so it wins over .skeleton.grid (equal specificity, later in source) —
   a hidden skeleton must stay hidden even while it carries the grid class. */
.skeleton[hidden] { display: none; }

/* ---- text block preview (square, grid only) ---- */
.item-preview { display: none; }

/* ---- connection badge ---- */
.connection-badge {
  font-size: var(--text-ui);
  color: var(--fg-muted);
}

/* ---- feed (list) ---- */
.feed {
  display: flex;
  flex-direction: column;
  transition-property: opacity;
  transition-duration: 120ms;
  transition-timing-function: cubic-bezier(0.2, 0, 0, 1);
}
.feed.view-switching {
  opacity: 0;
  transition-duration: 60ms;
  transition-timing-function: ease-in;
}
.feed.view-switching .item {
  transition: none !important;
}

.item {
  display: grid;
  grid-template-columns: 1fr auto;
  column-gap: var(--gutter);
  padding: var(--space-4) 0;
  border-bottom: 1px solid var(--border);
  /* Typewriter focus line for j/k (see moveFocus): scrollIntoView aligns the
     item's scroll-margin edge to the top of the viewport, so this offset is
     where the focused item comes to rest — about a quarter down, clear of the
     sticky topbar, with the item and the next one below it. Literal units (not
     a token) so the vh fallback works: via var() an unsupported svh would be
     invalid at computed-value time and collapse to 0 rather than to the line
     above. */
  scroll-margin-top: 28vh;
  scroll-margin-top: 28svh;
}
.item:first-child { padding-top: 0; }
.item:last-child { border-bottom: 0; }

.item-body {
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.item-title {
  font-size: var(--text-title);
  line-height: var(--leading);
  font-weight: 600;
  overflow-wrap: anywhere;
  word-break: break-word;
  text-wrap: balance;
}
.item-title a { color: var(--fg); }
.item-title a:hover { text-decoration: underline; }
.item-source {
  font-size: var(--text-ui);
  color: var(--fg-muted);
  margin-top: var(--space-2);
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  max-width: 100%;
}
.item-source a {
  color: inherit;
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.item-source a:hover { color: var(--fg); text-decoration: underline; }
.item-desc {
  margin-top: var(--space-2);
  color: var(--fg-secondary);
  font-size: var(--text-body);
  line-height: var(--leading);
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-wrap: pretty;
}
/* Single attribution row: leading stack (sharer over channel) + trailing
   timestamp/buttons group, vertically centered. */
.item-attribution {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding-top: var(--space-8);
  font-size: var(--text-ui);
  color: var(--fg-muted);
  font-variant-numeric: tabular-nums;
}
.avatar {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--border);
  flex-shrink: 0;
}
.avatar-fallback {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-micro);
  font-weight: 700;
  line-height: 1;
  color: var(--fg-muted);
  background: var(--surface);
  outline: 1px solid rgba(0, 0, 0, 0.08);
  outline-offset: -1px;
}
.favicon {
  width: 14px;
  height: 14px;
  border-radius: 2px;
  object-fit: contain;
  flex-shrink: 0;
  background: var(--surface);
  outline: 1px solid rgba(0, 0, 0, 0.08);
  outline-offset: -1px;
}
@media (prefers-color-scheme: dark) {
  .favicon, .avatar-fallback { outline-color: rgba(255, 255, 255, 0.1); }
}
.item-attribution .dot { opacity: 0.4; }
.item-attribution a { color: var(--fg-muted); }
.item-attribution a:hover { color: var(--fg); }
.item-attribution time { color: var(--fg-muted); }
/* Leading stack: sharer row over the channel/collection line */
.item-attr-lead {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.item-author {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}
.item-author a { color: var(--fg-secondary); }
.item-author a:hover { color: var(--fg); text-decoration: underline; }
/* Truncate a long sharer name (a link or a plain span) before it pushes the
   trailing buttons */
.item-author > :last-child {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.meta-channel {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}
.meta-channel:empty { display: none; }
.meta-channel a {
  color: var(--fg-muted);
  max-width: 24ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.meta-channel a:hover { color: var(--fg); }

.item-thumb-wrap {
  position: relative;
}
.item-thumb {
  width: 100px;
  height: 100px;
  object-fit: cover;
  background: var(--surface);
  border: none;
  outline: 1px solid rgba(0, 0, 0, 0.1);
  outline-offset: -1px;
}
/* Right-aligned meta group: timestamp + source/filter icon buttons */
.meta-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-shrink: 0;
}
/* Double only the timestamp → ✶✶ source gap back to 16px; the 8px flex gap
   handles every other pair in the group. 8px gap + 8px margin = 16px. */
.meta-actions time { margin-right: var(--space-2); }
/* inline actions button (... ellipsis) in meta line */
.item-actions {
  position: relative;
  flex-shrink: 0;
}
/* Icon buttons in the meta line: the Are.na source (✶✶) and the filter menu */
.item-actions-btn,
.item-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--control-h);
  height: var(--control-h);
  padding: 0;
  border: 1px solid var(--border-control);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--fg-muted);
  cursor: pointer;
}
.item-actions-btn:hover,
.item-icon-btn:hover {
  color: var(--fg);
  border-color: var(--border-active);
  text-decoration: none;
}
/* ✶✶ source mark sized to read like the 14px icons beside it */
.item-icon-btn {
  font-size: var(--text-sm);
  line-height: 1;
  letter-spacing: -0.08em;
  text-indent: -0.08em;
}
.item-actions-menu {
  /* [popover]: reset UA top-layer defaults, anchor to the trigger button
     (per-instance anchor-name/position-anchor pairs are set inline in JS) */
  margin: 0;
  padding: 0;
  inset: auto;
  bottom: calc(anchor(top) + var(--space-1));
  right: anchor(right);
  min-width: 160px;
  background: var(--bg);
  border: 1px solid var(--border-control);
  border-radius: var(--radius);
  overflow: hidden;
  font-size: var(--text-ui);
  line-height: var(--leading);
}
/* No enter/exit animation, by design. Are.na's Pulldown swaps state and the
   options are simply there — no fade, no slide, no shadow to imply a layer
   above the page. This also retires the transform/overlay/allow-discrete
   choreography that kept producing close-flash artifacts. */
/* Per-item add-filter menu only (the scope/sort/type dropdowns and the funnel
   panel reuse .item-actions-menu but not these rows). Cap the width so a long
   channel title or username truncates via the row span's ellipsis instead of
   stretching the popover. */
.item-actions-menu:has(.actions-menu-label) {
  max-width: 260px;
}
.actions-menu-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
}
.actions-menu-row span {
  flex: 1;
  min-width: 0;
  color: var(--fg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Square icon buttons on menu rows and filter rows (matches .icon-btn) */
.actions-menu-row button,
.filter-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: var(--control-h);
  height: var(--control-h);
  padding: 0;
  border: 1px solid var(--border-control);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--fg-muted);
  cursor: pointer;
}
.actions-menu-row button:hover,
.filter-remove:hover {
  color: var(--fg);
  border-color: var(--border-active);
}
.actions-menu-label {
  display: block;
  font-size: var(--text-label);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--fg-muted);
  padding: var(--space-2) var(--space-3) var(--space-1);
}
/* Even inset on all four edges of the popover: 12px sides come from the row/
   label horizontal padding, so match it at the top and bottom too. */
.actions-menu-label:first-child {
  padding-top: var(--space-3);
}
.actions-menu-row:last-child {
  padding-bottom: var(--space-3);
}
.actions-menu-label + .actions-menu-row {
  padding-top: var(--space-1);
}

/* ---- scope/sort/type dropdowns ---- */
/* Modeled directly on ervell's UI/Pulldown: the open options panel is not a
   floating menu, it is the lower half of the field. It sits flush on the
   trigger's bottom edge, bleeds 1px past each side so the two borders read as
   one continuous box, squares off the corners where they meet, and carries the
   same darkened edge the expanded trigger takes. No offset, no shadow, no
   motion.

   Outer edges align exactly with the field's. (ervell writes this as
   `margin: 0 -1px`, but that sits inside a fullWidth Overlay whose box is
   already inset by the container's border; on raw anchor positioning the same
   -1px just makes the panel 2px wider than its trigger.) The -1px on the
   opening edge instead pulls the panel over the trigger's border so the two
   1px lines collapse into a single seam rather than stacking into 2px.

   Pinning both left and right to the anchor also sizes the panel to the field,
   which is why the rightmost dropdown no longer needs a special right-aligned
   case to stay on-screen. We avoid position-try: re-evaluating fallbacks
   against an anchored popover made it jump off-screen. */
.filter-select-menu {
  top: calc(anchor(bottom) - 1px);
  bottom: auto;
  left: anchor(left);
  right: anchor(right);
  /* The UA sheet gives [popover] `width: fit-content`, which over-constrains
     left+right and silently drops the right inset. Restore auto so the panel
     actually spans its trigger. */
  width: auto;
  min-width: 0;
  border-color: var(--border-active);
  border-top-color: var(--border-control);
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}
/* Reset button defaults; layout/typography come from the shared .actions-menu-row
   class (and .item-actions-menu's font-size) this button also carries. */
.filter-select-option {
  width: 100%;
  border: 0;
  background: var(--bg);
  color: var(--fg);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
/* Match the trigger's metrics exactly — same horizontal padding, and a check
   that occupies the chevron's slot. In ervell the resting value and the open
   options are literally the same <Option> component, so the selected label
   must not shift by a pixel when the panel opens. */
.filter-select-option {
  padding-left: var(--space-3);
  padding-right: var(--space-2);
}
.filter-select-option:hover { background: var(--border); }
/* The label span reuses .actions-menu-row span (flex:1 + ellipsis). Pin the
   trailing check so it doesn't also claim flex space. */
.filter-select-option .opt-check {
  flex: 0 0 auto;
  display: inline-flex;
  color: var(--fg-muted);
  visibility: hidden;
}
.filter-select-option[aria-selected="true"] .opt-check { visibility: visible; }
.filter-select-option .opt-check svg { width: 0.875em; height: 0.875em; }

@media (max-width: 640px) {
  /* Height of the fixed bottom filter bar: select height (30px, set below)
     plus its top+bottom padding. Used to pad the feed and lift the toast
     region clear of the bar. */
  :root { --filter-bar-h: calc(30px + 2 * var(--space-4)); }
  .item { grid-template-columns: 1fr 60px; }
  .item-thumb { width: 60px; height: 60px; }
  /* Unwrap controls so the title-row icons join topbar-inner's flex layout.
     The three selects (.select-row) leave the topbar entirely for the fixed
     bottom bar below, so the top bar carries only the title + icon cluster. */
  .controls { display: contents; }
  .controls[hidden] { display: none; }
  /* Filters move to a fixed bottom bar to declutter the top: equal thirds
     that never wrap. When a third gets too narrow for the widest currently
     selected label, all three labels scale down together:
     font-size = (row width − 2 gaps − 3 borders) /
     (3 × (label width in em + 2.125em horizontal padding)).
     The 22px subtraction below = 2 × var(--space-2) (8) + ~6px of borders.
     The em padding and arrow gutter shrink with the font, so the fit
     holds at every width below the breakpoint. fitSelectLabels() in
     app.js measures the selected labels and sets --select-fit-divisor;
     the fallback covers the widest selectable label ("Attachments"). */
  .select-row {
    position: fixed;
    inset: auto;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 10;
    display: flex;
    align-items: normal;
    justify-content: normal;
    pointer-events: auto;
    min-width: 0;
    gap: var(--space-2);
    padding: var(--space-4) max(var(--space-4), env(safe-area-inset-right, 0))
             calc(var(--space-4) + env(safe-area-inset-bottom, 0))
             max(var(--space-4), env(safe-area-inset-left, 0));
    background: var(--bg);
    border-top: 1px solid var(--border);
    /* No container-type here: it would make this fixed bar the containing block
       for the dropdown popovers (position:fixed), so on close — as they leave
       the top layer — they'd snap under the field for a frame. The label-fit
       below uses the viewport width instead of a container query. */
    /* Slides down out of view in concert with the top bar (see .nav-hidden). */
    transition-property: transform;
    transition-duration: 200ms;
    transition-timing-function: cubic-bezier(0.2, 0, 0, 1);
  }
  /* Hide on scroll-down by pushing the bar below the viewport; mirrors
     .topbar.nav-hidden, which slides the top bar up. app.js toggles both
     classes together from the same scroll handler. */
  .select-row.nav-hidden {
    transform: translateY(100%);
    transition-duration: 150ms;
    transition-timing-function: ease-in;
  }
  /* Explicit display:flex above outranks the UA [hidden] rule, so restore it:
     the bar is hidden pre-auth (app.js mirrors .controls[hidden] onto it). */
  .select-row[hidden] { display: none; }
  .select-row .filter-select {
    flex: 1 1 0;
    min-width: 0;
  }
  .filter-select-btn { width: 100%; }
  /* The fields sit in the fixed bottom bar, so the panel joins the trigger's
     TOP edge and grows upward — the same Pulldown seam, mirrored. Swap which
     pair of corners squares off and which border edge stays light. */
  .filter-select-menu {
    top: auto;
    bottom: calc(anchor(top) - 1px);
    border-top-color: var(--border-active);
    border-bottom-color: var(--border-control);
    border-radius: var(--radius) var(--radius) 0 0;
  }
  .filter-select-btn[aria-expanded="true"] {
    border-radius: 0 0 var(--radius) var(--radius);
  }
  /* The panel is pinned to both of its trigger's edges, so it can't spill
     off-screen — the rightmost field needs no special case. */
  /* Icons stay on title row; push icon cluster to the right */
  #settings-toggle { margin-left: auto; }
  .controls .spacer { display: none; }
  /* Single icon row in the top bar now that selects live at the bottom. */
  .topbar-inner { column-gap: var(--space-2); row-gap: 0; }
  /* Pad the feed so the last cards / load-more clear the fixed filter bar. */
  .container { padding-bottom: calc(1em + var(--filter-bar-h) + env(safe-area-inset-bottom, 0)); }

  /* Larger touch targets on mobile */
  .filter-select-btn,
  .controls > button,
  .controls > a {
    font-size: var(--text-body);
    padding: var(--space-2) var(--space-3);
  }
  .filter-select-btn {
    /* See .select-row above: shrink all three labels together once an
       equal-width third can't fit the widest selected label. Left padding +
       (gap + chevron + right padding ≈ 1.5em) reproduce the old <select> box
       so the --select-fit-divisor constant (2.125em) still holds.
       100vw − 2×--space-4 is the bar's content width (it spans the viewport
       with --space-4 side padding) — the same value the old 100cqw resolved to. */
    font-size: min(var(--text-body), calc((100vw - 2 * var(--space-4) - 22px) / var(--select-fit-divisor, 23.25)));
    padding-left: var(--space-3);
    padding-right: var(--space-1);
    gap: var(--space-1);
  }
}

/* ---- feed (grid) ---- */
.feed.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--block-w), 1fr));
  gap: var(--gutter);
  justify-content: center;
}
.feed.grid .item {
  display: flex;
  flex-direction: column;
  gap: 0;
  padding: 0;
  border-bottom: 0;
  border: none;
  border-radius: 0;
  border: 1px solid var(--border);
  overflow: hidden;
}
/* Hover darkens the rule. Are.na blocks never lift off the page. */
.feed.grid .item:hover {
  border-color: var(--border-control);
}
.feed.grid .item:first-child { padding-top: 0; }
.feed.grid .item-thumb-wrap { order: -1; }
.feed.grid .item .item-preview {
  display: block;
  order: -1;
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border-bottom: 1px solid var(--border);
  padding: var(--space-4);
  background: var(--surface);
  font-size: var(--text-ui);
  line-height: var(--leading);
  color: var(--fg-secondary);
  word-break: break-word;
}
.feed.grid .item:has(.item-preview) .item-desc { display: none; }
.feed.grid .item-thumb {
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;
  /* contain (not cover) so images scale to fit the square frame without
     over-cropping; the base .item-thumb's var(--surface) background fills
     the letterbox area in both light and dark mode. */
  object-fit: contain;
  border: 0;
  border-bottom: 1px solid var(--border);
  outline: none;
}
@media (prefers-color-scheme: dark) {
  .item-thumb { outline-color: rgba(255, 255, 255, 0.1); }
}
/* Grid cards reuse the list's text styles and vertical spacing hierarchy;
   only horizontal/card insets are grid-specific. Horizontal insets use rem
   so the smaller-font attribution/meta rows align with the body text rather
   than getting a narrower em-based inset. */
.feed.grid .item-body {
  padding: var(--space-3) var(--space-3) 0;
}
.feed.grid .item-attribution {
  /* Pin the attribution to the bottom of the (row-stretched) card so cards
     align across a row regardless of neighbours' title/description length.
     The auto margin absorbs the extra height; the 2em padding-top becomes the
     *minimum* gap above the attribution. */
  margin-top: auto;
  padding: var(--space-8) var(--space-3) var(--space-3);
}
@media (max-width: 640px) {
  .feed.grid { grid-template-columns: 1fr; gap: var(--space-4); }
  .feed.grid .item { max-width: none; }
}

/* ---- load more ---- */
.loadmore-wrap { text-align: center; margin-top: 1em; }
#loadmore {
  font: inherit;
  font-size: var(--text-ui);
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--border-control);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--fg-secondary);
  cursor: pointer;
}
#loadmore:hover { border-color: var(--border-active); }
#loadmore:disabled { opacity: 0.4; cursor: default; }

/* Zero-height tripwire watched by the IntersectionObserver; sits just above
   the fallback button so scrolling near the end pre-fetches the next page. */
.feed-sentinel {
  height: 1px;
  pointer-events: none;
}
/* Shown in place of the button while the next page is fetching */
.loadmore-spinner {
  width: 18px;
  height: 18px;
  margin: 0 auto;
  border: 2px solid var(--border);
  border-top-color: var(--fg-muted);
  border-radius: 50%;
  animation: loadmore-spin 0.7s linear infinite;
}
.loadmore-spinner[hidden] { display: none; }
@keyframes loadmore-spin {
  to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .loadmore-spinner { animation-duration: 1.4s; }
}

.empty {
  text-align: center;
  color: var(--fg-muted);
  font-size: var(--text-body);
  padding: 3em 0;
}

/* ---- filtered items ---- */
/* Quiet exit when a new filter catches an item: fade out, then .filtered
   removes it from layout. Fade only — the drift read as decoration. */
.item.removing {
  opacity: 0;
  transition: opacity 200ms ease-in;
  pointer-events: none;
}
.item.filtered { display: none !important; }

/* ---- icon buttons (filter, settings) ---- */
.controls .icon-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--control-h);
  height: var(--control-h);
  flex-shrink: 0;
  padding: 0;
  border: 1px solid var(--border-control);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--fg-muted);
  cursor: pointer;
}
.controls .icon-btn::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 40px;
  height: 40px;
}
.controls .icon-btn:hover,
.controls a.icon-btn:hover { color: var(--fg); border-color: var(--border-active); text-decoration: none; }
.controls .icon-btn.connected { color: var(--fg-muted); }
.icon-badge[hidden] { display: none; }
.icon-badge {
  position: absolute;
  top: -6px;
  right: -6px;
  font-size: var(--text-micro);
  line-height: 14px;
  min-width: 14px;
  height: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--accent);
  color: var(--bg);
  border-radius: 7px;
  padding: 0 var(--space-1);
  font-weight: 700;
  /* Tabular figures so a 1→2 digit change doesn't shift the badge width */
  font-variant-numeric: tabular-nums;
  /* A quiet fade when the count hits zero (see .hiding). The count itself is
     the information; it doesn't need to pop to be noticed. */
  transition: opacity 150ms ease-out;
}
.icon-badge.disabled {
  background: var(--border);
  color: var(--fg-muted);
}
.icon-badge.hiding {
  opacity: 0;
}

/* ---- filter dropdown ---- */
#filter-btn { anchor-name: --filter-anchor; }
.filter-dropdown {
  /* [popover]: reset UA top-layer defaults, anchor below the filter button */
  margin: 0;
  padding: 0;
  inset: auto;
  position-anchor: --filter-anchor;
  /* Floating panel, not a joined Pulldown — stand it off the trigger so the
     two boxes read as separate rather than as one seamed control. */
  top: calc(anchor(bottom) + var(--space-1));
  right: anchor(right);
  width: 220px;
  max-height: 260px;
  /* Flex column with a clipped body so the header pins and only the list
     scrolls (see .filter-list). */
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bg);
  border: 1px solid var(--border-control);
  border-radius: var(--radius);
  font-size: var(--text-ui);
  line-height: var(--leading);
}
/* The display:flex above outranks the UA's `[popover]:not(:popover-open) {
   display: none }`, so the panel would never hide. Restore it explicitly —
   this is load-bearing, not leftover animation state. */
.filter-dropdown:not(:popover-open) {
  display: none;
}
.filter-dropdown-head {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--border);
  font-weight: 700;
  color: var(--fg-secondary);
}
.filter-dropdown-head[hidden] { display: none; }
.filter-head-btns {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.filter-reset-btn {
  font-family: inherit;
  font-size: var(--text-sm);
  line-height: 1;
  color: var(--fg-muted);
  background: none;
  border: none;
  padding: var(--space-2) var(--space-1);
  cursor: pointer;
}
.filter-reset-btn:hover { color: var(--fg); }
.filter-reset-btn[hidden] { display: none; }
.filter-toggle-btn {
  font-family: inherit;
  font-size: var(--text-sm);
  line-height: 1;
  color: var(--fg-muted);
  background: none;
  border: 1px solid var(--border-control);
  border-radius: var(--radius);
  padding: var(--space-2) var(--space-4);
  cursor: pointer;
}
.filter-toggle-btn:hover { border-color: var(--border-active); }
.filter-list {
  padding: var(--space-1) 0;
  /* Only the list scrolls (header stays pinned above); thin, themed scrollbar
     to match the app's native-minimal treatment. */
  overflow-y: auto;
  min-height: 0;
  scrollbar-width: thin;
}
.filter-empty {
  padding: var(--space-3);
  font-size: var(--text-ui);
  color: var(--fg-muted);
  line-height: var(--leading);
  white-space: pre-line;
}
.filter-section-label {
  font-size: var(--text-label);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--fg-muted);
  padding: var(--space-2) var(--space-3) var(--space-1);
}
.filter-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
}
.filter-row:hover { background: var(--surface); }
.filter-domain {
  flex: 1;
  font-size: var(--text-ui);
  color: var(--fg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* ---- mobile touch targets (after base rules so overrides apply) ---- */
@media (max-width: 640px) {
  /* Match the article-card icon buttons (✶✶ source, filter) to the nav's */
  .controls .icon-btn,
  .item-actions-btn,
  .item-icon-btn {
    width: 36px;
    height: 36px;
  }
  .filter-select-btn {
    height: 36px;
  }
}
/* Scale down controls on very narrow screens (< iPhone SE) */
@media (max-width: 374px) {
  .controls .icon-btn,
  .item-actions-btn,
  .item-icon-btn {
    width: 30px;
    height: 30px;
  }
  .filter-select-btn {
    height: 30px;
  }
  .topbar-inner {
    gap: var(--space-1);
  }
}

/* ---- toast ---- */
/* Brief, non-blocking confirmation for filter add/remove (issue #35). Stacks
   bottom-center; each toast auto-dismisses. Shadow over border for depth. */
.toast-region {
  position: fixed;
  left: 50%;
  bottom: calc(1.25em + env(safe-area-inset-bottom, 0));
  transform: translateX(-50%);
  z-index: 60;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  pointer-events: none;
}
/* Lift toasts above the fixed bottom filter bar on mobile. */
@media (max-width: 640px) {
  .toast-region {
    bottom: calc(1.25em + var(--filter-bar-h) + env(safe-area-inset-bottom, 0));
    /* Glide with the bar when it hides/reveals on scroll (see below). */
    transition: bottom 200ms cubic-bezier(0.2, 0, 0, 1);
  }
  /* When the bar is hidden on scroll it slides off-screen (.select-row.nav-hidden,
     reparented under <body> on mobile), so there's no bar to clear — drop toasts
     back to the bottom instead of leaving them floating where the bar used to be. */
  body:has(.select-row.nav-hidden) .toast-region {
    bottom: calc(1.25em + env(safe-area-inset-bottom, 0));
  }
}
.toast {
  pointer-events: auto;
  max-width: min(90vw, 320px);
  padding: var(--space-2) var(--space-3);
  background: var(--bg);
  border: 1px solid var(--border-control);
  color: var(--fg);
  border-radius: var(--radius);
  font-size: var(--text-ui);
  line-height: var(--leading);
  display: flex;
  align-items: center;
  gap: var(--space-2);
  /* Fade only. A toast has no ervell counterpart, so it follows the house
     rule: it may appear, but it may not perform. */
  opacity: 0;
  transition: opacity 180ms ease-in;
}
.toast-icon {
  display: inline-flex;
  align-items: center;
  color: var(--fg-muted);
  flex-shrink: 0;
}
.toast-label {
  flex: 1;
  min-width: 0;
  text-overflow: ellipsis;
  overflow: hidden;
  white-space: nowrap;
  text-align: left;
}
.toast-undo {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: none;
  background: none;
  color: var(--fg-muted);
  cursor: pointer;
  border-radius: var(--radius);
  flex-shrink: 0;
}
.toast-undo:hover {
  color: var(--fg);
}
.toast.show {
  opacity: 1;
  transition-duration: 220ms;
  transition-timing-function: ease-out;
}
