/* ─────────────────────────────────────────────────────────────────────────────
   style.css — Share page visuals. Mirrors the app's design tokens
   (src/theme/tokens.ts) and the VerticalCard / CountdownChip patterns
   so recipients see a near-continuous experience between the share link
   and the in-app view of the same list.
   ───────────────────────────────────────────────────────────────────────── */

:root {
  /* Tokens — kept in sync with app/src/theme/tokens.ts. */
  --bg: #F8F8F8;
  --card-bg: #FFFFFF;
  --ink: #1a1a1a;
  --ink-subtle: rgb(140, 133, 123);
  --separator: rgba(0, 0, 0, 0.08);
  --gold: #E6A817;
  --error: #B8603A;                              /* warm-dark-red — matches app tokens.error */
  --shadow-sm: 0 2px 4px rgba(60, 50, 40, 0.05);
  --shadow-md: 0 4px 14px rgba(0, 0, 0, 0.07);
  --shadow-lg: 0 8px 30px rgba(60, 50, 40, 0.18);

  /* Image well — matches the in-app polaroid border. The width clamps
     so it never dominates a narrow 2-col-on-mobile card. */
  --image-well: #F0F0F0;
  --image-border: #FFFFFF;
  --image-border-w: clamp(10px, 4.5%, 18px);

  --radius-card: 14px;
  --radius-lg: 22px;

  /* Fonts mirror Manrope (body) + EB Garamond (display) used in the app. */
  --font-body: 'Manrope', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;
  --font-display: 'EB Garamond', Georgia, 'Times New Roman', serif;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* iOS notch / Dynamic Island safe-area. Without this, the page content
   (banner OR the header when the banner is dismissed) draws under the
   status bar — "Shared by Emma W" gets clipped against the top edge.
   The body itself absorbs the inset so EITHER chrome path (banner-then-
   root OR root-only) lands below the notch. */
body {
  padding-top: env(safe-area-inset-top);
}

/* When a modal is open we lock the underlying page so it doesn't
   scroll behind the dialog. Body overflow:hidden alone leaves the
   document scrollable on iOS; pinning position fixes that. We don't
   restore the prior scroll offset on close — modals are short-lived
   and dropping back to scrollTop 0 reads as "fresh state" rather than
   "lost place" for this kind of page. */
body.scroll-locked {
  position: fixed;
  width: 100%;
  overflow: hidden;
}

a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; }

.root {
  max-width: 760px;
  margin: 0 auto;
  padding: 28px 18px 100px;                      /* bottom pad clears the sticky mobile CTA */
  min-height: 100vh;
}

/* ── Loading + error states ─────────────────────────────────────────────── */

.loading {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 80px 0;
}
.loading-dot {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 3px solid var(--separator);
  border-top-color: var(--ink);
  animation: spin 0.9s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.error-card {
  margin: 64px auto 0;
  max-width: 380px;
  padding: 28px 24px;
  background: var(--card-bg);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  text-align: center;
}
.error-card .title {
  font-family: var(--font-display);
  font-size: 26px;
  font-weight: 500;
  margin: 0 0 8px;
}
.error-card .body {
  font-size: 14px;
  color: var(--ink-subtle);
  line-height: 1.4;
  margin: 0;
}

/* ── Inactive-link state (disabled / rotated share token) ────────────────── */
.inactive-card {
  margin: 64px auto 0;
  max-width: 430px;
  padding: 40px 28px 30px;
  background: var(--card-bg);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  text-align: center;
}
.inactive-glyph {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: #EFEEEA;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto 22px;
}
.inactive-title {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 21px;
  line-height: 1.25;
  letter-spacing: 0.2px;
  margin: 0 0 9px;
  color: var(--ink);
}
.inactive-body {
  font-size: 14px;
  line-height: 1.6;
  color: var(--ink-subtle);
  margin: 0;
}

/* ── Mobile-only passive banner (top) ────────────────────────────────────
   Hidden on viewports ≥ 600px via @media. Native iOS smart banner
   (apple-itunes-app meta) requires a real App Store ID we don't have
   yet (Expo Go testing), so this custom banner carries the affordance.
   When the App Store listing lands, ADD the native banner alongside. */
.app-banner {
  display: none;                                 /* shown via @media */
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  background: var(--card-bg);
  border-bottom: 1px solid var(--separator);
}
.app-banner-icon {
  width: 36px; height: 36px; border-radius: 8px;
  background: var(--ink);
  display: flex; align-items: center; justify-content: center;
  color: #fff;
  font-family: var(--font-display);
  font-size: 22px; font-weight: 500;
}
.app-banner-body { flex: 1; min-width: 0; }
.app-banner-title {
  font-weight: 600;
  font-size: 13.5px;
  color: var(--ink);
}
.app-banner-sub {
  font-size: 11.5px;
  color: var(--ink-subtle);
  margin-top: 2px;
}
.app-banner-cta {
  background: var(--ink); color: #fff;
  border: none; border-radius: 999px;
  padding: 7px 14px;
  font-size: 12.5px;
  font-weight: 600;
}

/* ── Header ─────────────────────────────────────────────────────────────── */

.head {
  margin: 0 0 22px;
}
/* Top line — "Shared by" attribution with the "Open in app" pill
   nestled right beside it. flex-start (not space-between) so the pill
   sits next to the username rather than getting pushed to the page's
   right edge. */
.head-top {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 10px;
  margin: 0 0 8px;
}
.shared-by {
  margin: 0;
  font-size: 13px;
  color: var(--ink-subtle);
}

/* Open-in-app pill — softer than full ink so it doesn't dominate.
   Subtle warm-neutral fill, ink text, no border. Hover lifts the bg
   a touch without flipping to dark — keeps the calm tone. */
.open-in-app-pill {
  background: rgba(60, 55, 45, 0.07);
  color: var(--ink);
  border: none;
  border-radius: 999px;
  padding: 6px 14px;
  font-family: var(--font-body);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0;
  cursor: pointer;
}
.open-in-app-pill:hover { background: rgba(60, 55, 45, 0.12); }
/* Title row — chip + heading on one row, chip aligned to the title's
   first visual baseline. Wraps to a new line if the title is very
   short and the chip somehow runs out of room (rare, but keeps the
   chip from ever clipping). */
.title-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 0 0 4px;
}
.list-title {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(26px, 4.5vw, 34px);
  line-height: 1.15;
  letter-spacing: -0.3px;
  color: var(--ink);
  margin: 0;
  flex: 1 1 auto;
  min-width: 0;
}
.list-desc {
  font-size: 14px;
  color: var(--ink-subtle);
  margin: 0 0 10px;
  line-height: 1.5;
}

.meta-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding-top: 14px;
  border-top: 1px solid var(--separator);
}
.meta-group {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}
.meta-right { gap: 8px; }

/* Filter chip — mirrors ListDetailScreen.tsx fChip (line ~2024):
   hairline border, 999 radius, 11×5 padding, Manrope 500 12.5px in
   muted ink. Active state flips to ink bg / white text (fChipActive). */
.fchip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 5px 11px;
  border-radius: 999px;
  border: 0.5px solid rgba(0, 0, 0, 0.11);
  background: transparent;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 12.5px;
  color: rgba(60, 55, 45, 0.5);
  cursor: pointer;
  flex-shrink: 0;
  white-space: nowrap;
}
.fchip:hover { color: var(--ink); }
.fchip.active {
  background: var(--ink);
  border-color: var(--ink);
  color: #fff;
}
.fchip.active:hover { color: #fff; }
.fchip svg {
  width: 11px;
  height: 11px;
  flex-shrink: 0;
  stroke: rgba(60, 55, 45, 0.5);
}
.fchip.active svg { stroke: #fff; }

/* Inline × inside the active Sort chip — clears the active sort back
   to the default order. Tight margin-left so it tucks against the
   label; small hit area expansion via padding. */
.sort-clear {
  display: inline-flex;
  align-items: center;
  margin-left: 4px;
  margin-right: -2px;
  padding: 1px 2px;
  cursor: pointer;
}
.sort-clear svg {
  width: 11px;
  height: 11px;
  stroke: #fff;
  stroke-width: 2.4;
}

/* Sort dropdown — mirrors sdS in ListDetailScreen.tsx: 190 wide,
   white card with a soft shadow, top caret pointing back at the Sort
   chip. */
.sort-menu {
  position: absolute;
  width: 190px;
  background: #fff;
  border-radius: 12px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.13);
  padding: 4px 0;
  z-index: 220;
  font-family: var(--font-body);
}
.sort-menu-caret {
  position: absolute;
  top: -5px;
  left: 18px;
  width: 10px;
  height: 10px;
  background: #fff;
  transform: rotate(45deg);
}
.sort-menu-header {
  font-weight: 600;
  font-size: 10px;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: rgba(60, 55, 45, 0.42);
  padding: 10px 14px 4px;
}
.sort-menu-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 10px 14px;
  background: none;
  border: none;
  font-family: var(--font-body);
  font-size: 13px;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}
.sort-menu-row:hover { background: rgba(0, 0, 0, 0.03); }
.sort-menu-row.active .sort-menu-label { font-weight: 600; }
.sort-menu-row svg {
  width: 14px;
  height: 14px;
  stroke: var(--ink);
  stroke-width: 2.5;
  flex-shrink: 0;
}
.sort-menu-sep {
  height: 0.5px;
  background: rgba(0, 0, 0, 0.07);
  margin: 0 10px;
}
/* Countdown chip — mirrors the in-app date chip on ListDetailScreen's
   filter row (fChipDate, lines ~2047-2062): warm-tan pill with a
   hairline warm-brown border, warm-brown calendar glyph + label.
   Single combined string ("Sep 12 · 95d left") so the chip reads as
   one cohesive token rather than three sub-pieces. */
.countdown {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: #EDE6DA;
  border: 0.5px solid rgba(160, 120, 70, 0.2);
  border-radius: 999px;
  padding: 5px 11px;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 10.5px;
  color: #7a5020;
  letter-spacing: 0;
  white-space: nowrap;
  flex-shrink: 0;
}
.countdown svg {
  width: 11px;
  height: 11px;
  flex-shrink: 0;
  stroke: #B07828;
  stroke-width: 2;
}
.countdown-text { color: inherit; }

/* Compact ratio — "purchased / total" */
.item-count {
  font-size: 13px;
  color: var(--ink-subtle);
  font-weight: 500;
}

/* Refresh — icon-only on the right end of the meta row. Sized to
   match the X/Y claimed line height so the two right-end elements
   feel balanced. */
.refresh-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none; border: none;
  padding: 4px;
  color: var(--ink-subtle);
  border-radius: 6px;
  cursor: pointer;
}
.refresh-btn svg {
  width: 16px;
  height: 16px;
  stroke: currentColor;
  stroke-width: 2;
}
.refresh-btn:hover { background: rgba(0, 0, 0, 0.04); color: var(--ink); }
.refresh-btn:disabled { opacity: 0.55; cursor: default; }
.refresh-btn.refreshing svg { animation: spin 0.8s linear infinite; }

/* ── Item grid ──────────────────────────────────────────────────────────── */

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 16px;
  margin-top: 18px;
}
@media (max-width: 599px) {
  .grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 8px;
    margin-top: 14px;
  }
}

.card {
  background: var(--card-bg);
  border-radius: var(--radius-card);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  display: flex; flex-direction: column;
  transition: opacity 0.18s ease-out;
}

.image-wrap {
  position: relative;
  aspect-ratio: 1 / 1;
  background: var(--image-well);
  border: var(--image-border-w) solid var(--image-border);
  overflow: hidden;
}
.image-wrap img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.image-fallback {
  position: absolute; inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: clamp(28px, 6vw, 38px);
  color: #c0b5a5;
  font-family: var(--font-display);
}

/* OOS strip — matches RN VerticalCard.footBar (dark band over image foot). */
.oos-foot {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 5px 10px;
  background: rgba(26, 26, 26, 0.82);
  color: #F5F1EA;
  font-size: 9.5px;
  text-align: center;
  font-weight: 600;
  letter-spacing: 1.1px;
  text-transform: uppercase;
}

/* ── Card body — compact 3-row stack mirroring the in-app VerticalCard ───
   Row 1 Name      (EB Garamond 500, 13px / 16 line-height / -0.2 letter-spacing
                    / color rgb(31,28,24) — exact in-app values)
   Row 2 Brand · $Price  (Manrope 400, 11.5px, ink-subtle — same as in-app
                          metaRow which renders {brand}{price ? ' · ' + price : ''})
   Row 3 Mark | Buy ↗   (compact text links, separator above) */
.body {
  padding: 8px 10px 10px;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.body .name {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 13px;
  line-height: 16px;
  letter-spacing: -0.2px;
  color: rgb(31, 28, 24);                        /* exact in-app value */
  margin: 0;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.body .meta {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 11.5px;
  color: var(--ink-subtle);
  margin: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.body .actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 4px 10px;
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--separator);
}

/* Buy link — text + NE diagonal arrow matching the in-app
   ItemDetail "View on site" affordance: ink, 11.5px, hairline underline,
   11px box for the SVG arrow. */
.buy-link {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.1px;
  text-decoration: underline;
  text-decoration-color: var(--separator);
  text-underline-offset: 2px;
}
.buy-link:hover { text-decoration-color: var(--ink); }
.buy-link svg {
  width: 11px;
  height: 11px;
  flex-shrink: 0;
}

/* Claim row — matched-pair sibling of .buy-link on the LEFT. Same ink
   color, weight, and hairline underline so the two actions read as
   equally-weighted affordances on the row. Span carries the underline
   so the trailing checkmark glyph stays un-underlined. */
.claim-status {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 0;
  background: none; border: none;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.1px;
  font-family: var(--font-body);
  cursor: pointer;
}
.claim-status svg {
  width: 11px;
  height: 11px;
  flex-shrink: 0;
}
.claim-status span {
  text-decoration: underline;
  text-decoration-color: var(--separator);
  text-underline-offset: 2px;
}
.claim-status:hover span { text-decoration-color: var(--ink); }
.claim-status:disabled { opacity: 0.5; cursor: default; }

/* Purchased state — neutral, no green. Italic serif so it reads as a
   quiet status, not a button. Tiny check glyph via ::before mask so
   we don't need an extra DOM node. */
.claim-status-done {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 12.5px;
  color: var(--ink-subtle);
  letter-spacing: 0.2px;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
.claim-status-done::before {
  content: '';
  display: inline-block;
  width: 13px; height: 13px;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>") center/contain no-repeat;
          mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>") center/contain no-repeat;
}

/* Claimed card — whole card fades for the "done / inactive" feel. */
.card.claimed {
  opacity: 0.55;
}
.card.claimed:hover { opacity: 0.85; }            /* invites owner-side curiosity */
.card.claimed .actions {
  justify-content: center;
  border-top-color: transparent;
}

/* ── Refreshing card — soft white tint over image + centered pill ──────
   1:1 with the in-app VerticalCard refreshing state. The whole card is
   tappable (no action buttons inside while refreshing); tap shows the
   "This item is getting a refresh, check back soon" toast via JS. */
.card.refreshing { cursor: pointer; }
.card.refreshing .image-wrap::after {
  content: '';
  position: absolute;
  inset: var(--image-border-w);
  background: rgba(255, 255, 255, 0.6);
  border-radius: 4px;
  pointer-events: none;
}
.refresh-pill {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: rgba(255, 255, 255, 0.94);
  border: 1px solid rgba(0, 0, 0, 0.08);
  border-radius: 999px;
  padding: 5px 12px;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 11px;
  color: var(--ink);
  letter-spacing: 0.9px;
  text-transform: uppercase;
  z-index: 2;
  white-space: nowrap;
}
.refresh-pill svg { width: 13px; height: 13px; }

/* ── Sticky mobile-only "Open in app" pinned to viewport bottom ─────────── */
.sticky-cta {
  display: none;                                 /* shown via @media */
  position: fixed;
  bottom: 0; left: 0; right: 0;
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
  background: linear-gradient(180deg, rgba(248, 248, 248, 0) 0%, rgba(248, 248, 248, 1) 30%);
  z-index: 90;
}
.sticky-cta button {
  width: 100%;
  background: var(--ink);
  color: #fff;
  border: none; border-radius: 999px;
  padding: 14px;
  font-size: 14px;
  font-weight: 600;
  box-shadow: var(--shadow-md);
}

/* ── Modal scaffolding ──────────────────────────────────────────────────── */
.modal-backdrop {
  position: fixed; inset: 0;
  background: rgba(0, 0, 0, 0.45);
  display: none;
  align-items: flex-end;                         /* bottom-sheet by default on mobile */
  justify-content: center;
  z-index: 200;
}
.modal-backdrop.open { display: flex; }
@media (min-width: 600px) {
  .modal-backdrop { align-items: center; }
}
/* `modal-center` overrides default bottom-sheet — centers on EVERY
   viewport (used for QR + deep-link fallback: short content, mid-page
   focus reads better than a bottom sheet on a phone). */
.modal-backdrop.modal-center {
  align-items: center;
  padding: 20px;
}
.modal-card {
  position: relative;                            /* anchor for the close × */
  background: var(--card-bg);
  border-radius: 16px 16px 0 0;
  padding: 24px 22px 28px calc(22px + env(safe-area-inset-left));
  width: 100%;
  max-width: 460px;
  box-shadow: var(--shadow-lg);
  animation: slideUp 0.25s ease-out;
}
@media (min-width: 600px) {
  .modal-card { border-radius: 18px; padding: 28px; }
}
.modal-backdrop.modal-center .modal-card {
  border-radius: 18px;                           /* never bottom-anchored */
}
@keyframes slideUp {
  from { transform: translateY(20px); opacity: 0; }
  to   { transform: translateY(0);    opacity: 1; }
}
.modal-title {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 22px;
  margin: 0 0 8px;
  letter-spacing: -0.2px;
  color: var(--ink);
}
.modal-body {
  font-size: 14px;
  color: var(--ink-subtle);
  margin: 0 0 18px;
  line-height: 1.5;
}
.modal-input {
  width: 100%;
  padding: 12px 14px;
  font-family: inherit;
  font-size: 15px;
  border: 1px solid var(--separator);
  border-radius: 10px;
  background: #FAFAFA;
  outline: none;
  margin-bottom: 8px;
}
.modal-input:focus { border-color: var(--ink); background: #fff; }
.modal-error {
  font-size: 12px;
  color: var(--error);
  min-height: 16px;
  margin: 0 0 8px;
}
.modal-actions {
  display: flex; gap: 10px; justify-content: flex-end;
  margin-top: 6px;
}
.modal-btn {
  border: none;
  border-radius: 999px;
  padding: 10px 18px;
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
  font-family: inherit;
}
.modal-btn-cancel {
  background: rgba(0, 0, 0, 0.06);
  color: var(--ink);
}
.modal-btn-cancel:hover { background: rgba(0, 0, 0, 0.1); }
.modal-btn-primary {
  background: var(--ink);
  color: #fff;
}
.modal-btn-primary:hover { background: #000; }
.modal-btn-primary:disabled { opacity: 0.6; cursor: default; }

/* ── QR modal (desktop "Open in app") ────────────────────────────────────
   One header, QR + scan line, OR divider, copy-link inline hyperlink.
   "this share link" inside the descriptive sentence IS the action — no
   separate button. */
.qr-modal-card { text-align: center; }
.qr-modal-card .modal-title { margin-bottom: 18px; }
.qr-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 4px 0 0;
}
.qr-box {
  width: 180px; height: 180px;
  background: #fff;
  border: 1px solid var(--separator);
  border-radius: 12px;
  padding: 12px;
  display: flex; align-items: center; justify-content: center;
  margin-bottom: 12px;
}
.qr-box svg { width: 100%; height: 100%; }
.qr-instruction {
  font-size: 13px;
  color: var(--ink-subtle);
  margin: 0;
  line-height: 1.5;
}
.or-divider {
  display: flex; align-items: center; gap: 12px;
  margin: 20px 0 16px;
  color: var(--ink-subtle);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 1.2px;
}
.or-divider::before,
.or-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--separator);
}
.qr-alt {
  font-size: 13px;
  color: var(--ink-subtle);
  margin: 0 0 4px;
  line-height: 1.5;
}
.qr-link {
  color: var(--ink);
  font-weight: 600;
  text-decoration: underline;
  text-decoration-color: var(--ink-subtle);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  cursor: pointer;
}
.qr-link:hover { text-decoration-color: var(--ink); }

/* ── Deep-link fallback sheet (mobile, app not installed) ─────────────────
   Title-only sheet — no body paragraph. The headline IS the pitch
   ("save this list + share your own"); CTAs stay short and light. */
/* Deep-link fallback — brand mark anchors the top, headline + sub-line
   give vertical hierarchy, primary CTA full-width with the secondary
   collapsing to a quiet text link. Modal stays small and feels like
   a crafted moment instead of a wall of serif. */
.deeplink-attempt {
  text-align: center;
  max-width: 360px;          /* tighter than default 460 — keeps the
                                modal from feeling lonely on mobile */
  padding: 28px 26px 22px;
}
@media (min-width: 600px) {
  .deeplink-attempt { padding: 32px 30px 24px; }
}

/* Brand mark — matches .app-banner-icon (36px ink tile with "U"), sized
   up slightly to act as the modal's anchor. */
.deeplink-mark {
  width: 44px;
  height: 44px;
  margin: 0 auto 18px;
  border-radius: 12px;
  background: var(--ink);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 24px;
  letter-spacing: -0.5px;
  line-height: 1;
}

.deeplink-attempt .modal-title {
  font-size: 22px;
  line-height: 1.25;
  margin: 0 0 6px;
}

.deeplink-sub {
  font-family: var(--font-body);
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--ink-subtle);
  margin: 0 0 24px;
  letter-spacing: 0;
}

.deeplink-actions {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 4px;
}
.deeplink-primary {
  padding: 13px 18px;
  font-size: 14px;
}

/* Quiet secondary — plain text link instead of a competing pill. */
.deeplink-stay {
  background: none;
  border: none;
  padding: 10px 8px;
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 500;
  color: var(--ink-subtle);
  cursor: pointer;
  letter-spacing: 0;
}
.deeplink-stay:hover { color: var(--ink); }

/* Top-right × inside any modal-card. Calm by default (ink-subtle on
   transparent), brightens on hover. Sized to a 32px hit area so it's
   tappable without needing a visible chip around it. */
.modal-close-x {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 32px;
  height: 32px;
  border: none;
  background: none;
  border-radius: 999px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-subtle);
  cursor: pointer;
  padding: 0;
}
.modal-close-x:hover {
  background: rgba(0, 0, 0, 0.05);
  color: var(--ink);
}
.modal-close-x svg {
  width: 14px;
  height: 14px;
  stroke: currentColor;
  stroke-width: 2;
}

/* ── Toast — bottom-centered, white pill, auto-dismiss ─────────────────
   Matches the in-app Toast.tsx design (cardBg surface, ink text, warm
   3-layer shadow, min-height 44, radius 14, Manrope 500 13px). The
   prior dark pill drifted from the app system and competed with the
   sticky "Open in app" CTA for attention on the same row. */
.toast {
  position: fixed;
  left: 14px;
  right: 14px;
  bottom: 32px;
  margin: 0 auto;
  width: max-content;
  max-width: calc(100% - 28px);
  background: var(--card-bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 13px;
  line-height: 17px;
  letter-spacing: -0.1px;
  padding: 11px 14px;
  min-height: 44px;
  border-radius: 14px;
  display: flex;
  align-items: center;
  /* Warm-brown 3-layer-ish shadow matching the in-app toast pill — gives
     more visual lift than the standard card shadow since the toast
     floats over scrollable content. */
  box-shadow: 0 8px 18px rgba(60, 50, 40, 0.20);
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.18s ease-out, transform 0.18s ease-out;
  pointer-events: none;
  z-index: 300;
}
.toast.show {
  opacity: 1;
  transform: translateY(0);
}
/* On mobile the sticky "Open in app" CTA pins to the viewport bottom
   (~80 px tall plus the safe-area inset). The toast must sit above it
   so the two don't overlap. Mobile-only override keeps desktop at the
   default 32 px bottom (no sticky CTA on desktop). */
@media (max-width: 599px) {
  .toast {
    /* Sit ~8-10px above the sticky "Open in app" CTA. The CTA's own
       block is ~68px tall (12 top-padding + 44 button + 12 bottom-padding)
       plus the inset; we add a short cushion on top so the toast doesn't
       kiss the button.  */
    bottom: calc(78px + env(safe-area-inset-bottom));
  }
}

/* ── Empty state ────────────────────────────────────────────────────────── */
.empty-grid {
  grid-column: 1 / -1;
  text-align: center;
  padding: 40px 0;
  font-size: 14px;
  color: var(--ink-subtle);
}

/* ── Media queries ─────────────────────────────────────────────────────── */
@media (max-width: 599px) {
  .app-banner { display: flex; }
  .sticky-cta { display: block; }
  .open-in-app-pill { display: none; }          /* sticky CTA + top banner cover the action on mobile */
  .root { padding: 22px 14px 110px; }
}
