:root {
  --bg: #ffffff;
  --bg-2: #f7f8fa;
  --bg-3: #eef1f5;
  --line: #e3e7ed;
  --line-2: #c8d0da;
  --text: #12161d;
  --muted: #5b6675;
  /* 4.6:1 on white. Was #8b95a3 at 3.03:1 — below AA for body text, and it
     is used for the card micro-labels, which are as small as text gets here. */
  --dim: #6b7686;
  --accent: #0d9488;
  /* A fill dark enough to carry white label text. --accent itself is 3.74:1
     against white, under the 4.5 AA needs, and the gate button is a solid
     block of it with the only words on the page that must be read. Only the
     light theme needs the shift; on the dark grounds the accent is already
     bright enough to take dark text. */
  --accent-strong: #0f766e;
  --accent-2: #0369a1;
  --good: #15803d;
  --warn: #b45309;
  --bad: #be123c;
  /* Price RAISE red — deliberately DARKER than the airdrop --bad, so a mid-mint
     raise reads as its own, heavier warning and never blurs into an airdrop. */
  --raise: #85122e;
  --purple: #6d28d9;
  --radius: 10px;
  /* Chart series get their own tokens rather than borrowing --accent-2 and
     --warn. Those two are the SAME value in the orchard theme (#e8b62e), so
     the floor line and the sales line came out identical and the chart read as
     one series. Semantic tokens are chosen for meaning; a chart needs them
     chosen for contrast against each other. */
  --chart-sales: #0369a1;
  --chart-floor: #b45309;
  /* Alerts driven by very few buyers. Cannot borrow --warn: that is the
     same #e8b62e as --chart-sales in the orchard theme, so the flag would
     be invisible against the very colour it is meant to contrast with. */
  --alert-narrow: #b45309;
  /*
   * KIND HUES THAT HAD TO BECOME TOKENS.
   *
   * HEATING UP and MARKET OPEN are the two Forest kinds. Heating carried a
   * single hard-coded #f97316 on its filter chip and NO colour at all on its
   * row tag; market_open, added 2026-08-20, was never given either — so its
   * chip fell back to --hue's `var(--dim)` default and its row tag fell back
   * to the plain .alert-tag colour, which is exactly where heating's row tag
   * already was. Two Forest kinds, one indistinguishable pair of tags.
   *
   * They are tokens rather than literals because one hex cannot clear 4.5:1
   * on BOTH a white ground and two near-black ones: #f97316 measures 2.64:1
   * on the light tray and 6.23:1 on the dark. Per-theme values are the only
   * honest way to keep the same meaning readable everywhere.
   *
   * Light values (on --bg-2 / --bg-3 / --bg):
   *   heat   #c2410c → 4.87 / 4.57 / 5.18
   *   market #1d4ed8 → 6.31 / 5.92 / 6.70
   * Market blue is deliberately outside the family already in use — greens
   * (minting, earlydemand), teal (confirmed), amber (topsignal), orange
   * (heating), violet (floor_lifter), rose (price_raise, sellout).
   */
  --kind-heat: #c2410c;
  --kind-market: #1d4ed8;
  /*
   * CHECK-IN, the third Forest kind. Muted plum (~310°), the one gap left in
   * the wheel the tray already uses: greens 145–175 (minting, earlydemand,
   * confirmed), ambers 25–45 (heating, topsignal, paidcrowd), blues 199–220
   * (riser, market_open), violet 258 (floor_lifter), rose 350 (price_raise,
   * sellout). Low chroma on purpose — a check-in reports, it does not shout,
   * and it must not compete with the alert it is following up on.
   *   light  #8a4a7f → 5.87 / 5.51 / 6.24 on --bg-2 / --bg-3 / --bg
   *   dark   #c98fc2 → 6.81 / 6.09 / 7.27
   *   orchard #c98fc2 → 6.24 / 5.43 / 6.80 on the forest ground
   */
  --kind-checkin: #8a4a7f;
  --mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
  --head-font: system-ui, -apple-system, "Segoe UI", sans-serif;
}

/*
 * Themes. Everything above is the light default; each block below only
 * redefines the tokens, so no component needs to know which theme is on.
 * The choice is stamped on <html> and saved per browser — same idea as star
 * and hide, a preference belonging to the person looking, not to the data.
 */
html[data-theme="dark"] {
  --bg: #0f1319;
  --bg-2: #151a22;
  --bg-3: #1d242e;
  --line: #262f3b;
  --line-2: #3a4655;
  --text: #e6ebf2;
  --muted: #9aa7b8;
  --dim: #728091;   /* 4.62:1 — was #6b7889 at 4.14 */
  --accent: #2dd4bf;
  --accent-strong: #2dd4bf;   /* bright on a dark ground; the label is dark */
  --accent-2: #38bdf8;
  --good: #4ade80;
  --warn: #fbbf24;
  --bad: #fb7185;
  /* Deeper, more saturated than --bad so it still reads "darker" on the dark
     ground while staying above 4.5:1 for the bold label. */
  --raise: #e11d48;
  --purple: #a78bfa;
  --chart-sales: #38bdf8;
  --alert-narrow: #fbbf24;   /* amber vs sky riser, rose sellout */
  --chart-floor: #fbbf24;
  /* 6.23:1 on --bg-2, 5.57 on the hover ground. The original chip orange
     keeps its look here — only the light theme needed it darkened. */
  --kind-heat: #f97316;
  /* 6.87:1 / 6.15:1. Lighter and less cyan than --accent-2's #38bdf8, so the
     MARKET OPEN tag never reads as a focus ring or a chart line. */
  --kind-market: #60a5fa;
  --kind-checkin: #c98fc2;   /* 6.81:1 / 6.09:1 — plum, clear of the violet buying-wave tag */
}

/*
 * Fruit Trees. The palette is lifted from the collection's own site: the
 * canopy greens and oak browns the pixel art is built from, on the same dark
 * forest ground, with Golden Apple as the accent. Corners are squared off and
 * headings set in a pixel-friendly mono because the art is 32x32 sprites —
 * rounded cards would fight it.
 */
html[data-theme="orchard"] {
  --bg: #141c12;
  --bg-2: #1b2418;
  --bg-3: #22301d;
  --line: #3c5232;
  --line-2: #4f6b41;
  --text: #ece7d6;
  --muted: #9fb392;
  --dim: #748b68;   /* 4.68:1 — was #6f8564 at 4.33 */
  --accent: #6fae52;        /* Summer Green canopy */
  --accent-strong: #6fae52; /* bright on a dark ground; the label is dark */
  --accent-2: #e8b62e;      /* Golden Apple */
  --good: #6fae52;
  --warn: #e8b62e;
  /* Apple, lifted to 4.61:1. The collection's own #c8342e reads at 3.31 on
     this ground, and it carries SUPPLY CUT — the one label that must not be
     hard to read. */
  --bad: #d85b56;
  --raise: #b23b36;         /* deeper apple than --bad, still 4.5:1 on the forest ground */
  --purple: #b89ae0;        /* Void canopy */
  --radius: 0px;            /* pixel art has no rounded corners */
  --head-font: var(--mono);
  --chart-sales: #e8b62e;   /* Golden Apple */
  --chart-floor: #b89ae0;   /* Void canopy */
  --alert-narrow: #b89ae0;  /* Void purple: gold is already the riser here */
  --kind-heat: #f97316;     /* 5.71:1 on the forest ground */
  /* 7.25:1. The one cool colour in a warm palette, which is the point: this
     board's Void purple (#b89ae0) is already spoken for by --alert-narrow. */
  --kind-market: #7cb2f2;
  /* 6.24:1. Plum reads as a cousin of the Void canopy without being it —
     #b89ae0 is already --alert-narrow and the buying-wave tag. */
  --kind-checkin: #c98fc2;
}
html[data-theme="orchard"] .brand,
html[data-theme="orchard"] th {
  font-family: var(--head-font);
  letter-spacing: .04em;
  text-transform: uppercase;
}
html[data-theme="orchard"] .brand { color: var(--accent-2); }
html[data-theme="orchard"] img { image-rendering: pixelated; }

/* The theme switcher itself: icon-only, so it stays out of the way. */
/* gap, because the bell sits here on desktop too — without it the button
   welded itself to the theme switcher. */
.barend { flex: 1 1 0; display: flex; justify-content: flex-end; align-items: center; gap: 8px; position: relative; z-index: 2; }
.tabs.theme button { padding: 4px 9px; font-size: 13px; line-height: 1.4; }

* { box-sizing: border-box; }

/*
 * Everything a touch smaller on phones.
 *
 * The board and its chrome were sized for comfortable desktop reading; on a
 * phone that left the header icons and cards feeling oversized, and some rows
 * not quite fitting. A single ROOT zoom shrinks the whole interface uniformly —
 * every current browser honours `zoom`, and unlike transform:scale it reflows
 * cleanly and leaves position:fixed intact — so the careful mobile layout and
 * every proportion are untouched, just rendered ~10% smaller so more fits.
 */
@media (max-width: 640px) { html { zoom: 0.9; } }

/*
 * No browser-invented text sizes, anywhere.
 *
 * Opera Android (and Chrome's accessibility text scaling) inflate fonts on
 * pages that do not opt out. The header's wordmark is nowrap, so inflation
 * widened the first row past the viewport: the logo showed half-clipped and
 * the wallet-tray bell sat OFF-SCREEN until you panned — on the exact same
 * build that measured pixel-perfect at 360px in a browser without inflation.
 * The prefixed form alone was buried on body inside a media block; the
 * standard property on html is what Opera actually honours.
 */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
  -webkit-font-smoothing: antialiased;
}

button, input, select, textarea { font: inherit; color: inherit; }

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

.mono { font-family: var(--mono); font-size: 12px; }
.muted { color: var(--muted); }
.dim { color: var(--dim); }
.nowrap { white-space: nowrap; }

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

header {
  position: sticky; top: 0; z-index: 30;
  /* Follows the theme. This was hardcoded white, which stayed white behind the
     dark and orchard palettes. */
  background: color-mix(in srgb, var(--bg) 93%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
}

/* The title row and the filter row are different kinds of thing — one is
   identity and status, the other is controls you operate. Running them together
   with no separation read as one undifferentiated block. A hairline and a tint
   on the controls row is enough to say "these are the knobs". */
.bar {
  display: flex; align-items: center; gap: 16px; padding: 12px 20px; flex-wrap: wrap;
  border-bottom: 1px solid var(--line);
}

/* Brand and the theme switcher take equal flexible space on either side, so
   the status pills land in the true centre of the bar rather than being
   shoved right by a margin. min-width:0 lets a long name ellipsize instead of
   pushing the pills off-centre. */
/* Centre, not baseline: the wordmark now sits beside a 34px image, and
   baseline alignment would hang the tree off the text's baseline instead of
   centring it against the mark — the same alignment the main site uses. */
.brand { display: flex; align-items: center; gap: 10px; flex: 1 1 0; min-width: 0; overflow: hidden; position: relative; z-index: 1; }
.brand-logo { width: 34px; height: 34px; flex: none; }
/* Truncate rather than push: under an inflated text size or a zoomed webview the
   wordmark must give way with an ellipsis, never grow into the header icons. */
.brand-name { font-weight: 700; font-size: 17px; letter-spacing: 1px; white-space: nowrap; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.brand small { color: var(--dim); font-weight: 400; font-size: 12px; white-space: nowrap; }

.pills { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; flex: 0 1 auto; }

.pill {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--bg-2); border: 1px solid var(--line);
  border-radius: 999px; padding: 4px 11px; font-size: 12px; color: var(--muted);
}
.pill b { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }

.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--dim); }
.dot.live { background: #22c55e; box-shadow: 0 0 0 0 rgba(34, 197, 94, .55); animation: pulse 2s infinite; }
.dot.err { background: #ef4444; }
/* Behind the chain but still working — amber, not red: the scan is running and
   will close the gap on its own. Red is reserved for a watcher that has stopped. */
.dot.warn { background: var(--warn); }
.pill.is-behind { border-color: var(--warn); color: var(--warn); }
@keyframes pulse {
  70% { box-shadow: 0 0 0 6px rgba(34, 197, 94, 0); }
  100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
}

/* ---------- add bar ---------- */

.addbar { padding: 0 20px 12px; }
.addbar form { display: flex; gap: 8px; align-items: stretch; }
.addbar input {
  flex: 1; min-width: 0;
  background: var(--bg); border: 1px solid var(--line-2); border-radius: 8px;
  padding: 9px 12px; outline: none;
}
.addbar input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(13, 148, 136, .12); }
.addbar .btn { white-space: nowrap; }

#addMsg:not(:empty) {
  /* Inset to match the board and the .hidden-bar. #addMsg is a direct child of
     the padding-less <header>, so with only a top margin it ran edge-to-edge —
     a full-bleed pill with rounded corners flush to both browser edges. Side
     margins make it a tidy inset notice like everything else. */
  margin: 8px 20px; font-size: 13px; padding: 9px 12px; border-radius: 8px;
  border: 1px solid var(--line); background: var(--bg-2); color: var(--muted);
}
@media (max-width: 767px) { #addMsg:not(:empty) { margin: 8px 12px; } }
#addMsg.err { background: color-mix(in srgb, var(--bad) 12%, transparent); border-color: color-mix(in srgb, var(--bad) 35%, transparent); color: var(--bad); }
#addMsg.ok { background: color-mix(in srgb, var(--good) 12%, transparent); border-color: color-mix(in srgb, var(--good) 35%, transparent); color: var(--good); }
#addMsg .choice {
  display: block; margin-top: 6px; font-family: var(--mono); font-size: 12px;
  cursor: pointer; color: var(--accent-2);
}
#addMsg .choice:hover { text-decoration: underline; }

/* ---------- empty state ---------- */

.blank { max-width: 620px; margin: 60px auto; text-align: center; color: var(--muted); }
.blank h2 { color: var(--text); font-size: 20px; margin: 0 0 10px; letter-spacing: -.3px; }
.blank p { margin: 0 0 18px; line-height: 1.65; }
.blank .examples {
  text-align: left; background: var(--bg-2); border: 1px solid var(--line);
  border-radius: 10px; padding: 14px 16px; font-size: 13px;
}
.blank .examples div { margin: 5px 0; font-family: var(--mono); font-size: 12px; color: var(--muted); }
.blank .examples b { color: var(--text); font-family: inherit; font-size: 13px; }

.star {
  background: none; border: 0; color: var(--line-2); cursor: pointer;
  font-size: 17px; line-height: 1; padding: 3px 6px; border-radius: 5px;
}
.star:hover { background: color-mix(in srgb, var(--warn) 18%, transparent); color: var(--warn); }
.star.on { color: #f59e0b; }

/* ---------- controls ---------- */

.controls {
  display: flex; gap: 8px; padding: 10px 20px; flex-wrap: wrap; align-items: center;
  background: var(--bg-2);
}

.controls input[type=search], .controls select {
  background: var(--bg); border: 1px solid var(--line-2);
  border-radius: 8px; padding: 7px 10px; outline: none;
}
.controls input[type=search] { min-width: 240px; }
.controls input:focus, .controls select:focus { border-color: var(--accent-2); box-shadow: 0 0 0 3px rgba(3, 105, 161, .12); }

.tabs { display: flex; gap: 2px; background: var(--bg-3); border: 1px solid var(--line); border-radius: 8px; padding: 3px; }
.tabs button {
  background: none; border: 0; padding: 5px 13px; border-radius: 6px;
  color: var(--muted); cursor: pointer;
}
.tabs button.on { background: var(--bg); color: var(--text); font-weight: 550; box-shadow: 0 1px 2px rgba(16, 24, 40, .08); }

.btn {
  background: var(--bg); border: 1px solid var(--line-2); border-radius: 8px;
  padding: 7px 13px; cursor: pointer; color: var(--muted);
}
.btn:hover { border-color: var(--dim); color: var(--text); }
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
.btn.primary:hover { filter: brightness(1.06); }
.btn:disabled { opacity: .5; cursor: default; }

/* ---------- table ---------- */

main { padding: 0 20px 60px; }

/*
 * table-layout: fixed, and it is a performance fix, not a cosmetic one.
 *
 * Under the default `auto`, the browser measures every cell in every row to
 * work out column widths — so inserting the chart's row into a 381-row table
 * cost 191ms of relayout before a single pixel of chart was drawn. That is the
 * lag on opening a graph.
 *
 * Fixed layout takes the widths from the header row alone, which COLUMNS now
 * declares, so adding a row is O(1) instead of O(rows).
 */
table { width: 100%; border-collapse: collapse; table-layout: fixed; }
/*
 * With fixed layout a long name would otherwise spill into the next column —
 * but only text columns get clipped. Applying this to every cell put an
 * ellipsis after the star and the ✕, because a button is not text that can be
 * truncated and the browser drew the marker anyway.
 */
tbody td:not(.act) { overflow: hidden; text-overflow: ellipsis; }
thead th.act, tbody td.act { overflow: visible; text-align: center; padding-left: 2px; padding-right: 2px; }

thead th {
  /* --stick-top is measured from the live header height in app.js (setStickTop).
     The header wraps to 2–3 rows by width, so a fixed 108px pinned the column
     labels UNDER the 166–208px header and hid them on scroll. The var tracks the
     real height; 108px is only the pre-JS fallback. */
  position: sticky; top: var(--stick-top, 108px); z-index: 10;
  background: var(--bg);
  text-align: left; font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .6px; color: var(--dim);
  padding: 10px 10px; border-bottom: 1px solid var(--line); white-space: nowrap;
}
thead th.num, td.num { text-align: right; font-variant-numeric: tabular-nums; }

/* sortable headers */
thead th.sortable { cursor: pointer; user-select: none; }
thead th.sortable:hover { color: var(--text); background: var(--bg-2); }
thead th .arrow { opacity: 0; margin-left: 4px; font-size: 10px; }
thead th.sortable:hover .arrow { opacity: .35; }
thead th.sorted { color: var(--accent-2); }
thead th.sorted .arrow { opacity: 1; }

tbody tr { border-bottom: 1px solid var(--bg-3); cursor: pointer; }
tbody tr:hover { background: var(--bg-2); }
tbody tr.sel { background: color-mix(in srgb, var(--accent-2) 14%, transparent); }
tbody td { padding: 9px 10px; vertical-align: middle; }

/*
 * With a change window on, cells align by their FIRST LINE, not their middles.
 *
 * The deltas make cells different heights — Minted carries a value, a delta
 * and a progress bar, Floor carries a value and a delta, Mint take carries
 * just a value. Centring each of those against the row's height puts their
 * first lines at three different heights, so reading across a row the numbers
 * visibly stepped up and down and the table stopped looking like a table.
 *
 * Aligning to the top puts every primary value on one line across the whole
 * row, which is the only thing the eye is tracking. Scoped to has-change so a
 * board without the deltas keeps the centred look it always had.
 */
body.has-change tbody td { vertical-align: top; }
/*
 * The controls align to the first line too.
 *
 * They were left centred on the grounds that they are not numbers, which was
 * wrong: with the numeric cells pinned to the top, the Ask AI star and the
 * graph dropdown floated half a row below the text beside them and the right
 * end of every row looked detached from the left. Everything a reader scans
 * across should start on the same line, controls included.
 */
body.has-change tbody td.act { vertical-align: top; }
/* The collection cell keeps its own centring: the artwork is taller than the
   text line, and topping it out would strand the name against the image. */
body.has-change tbody td:first-child { vertical-align: middle; }

.coll { display: flex; align-items: center; gap: 10px; min-width: 0; }
.coll img, .thumb-ph {
  width: 34px; height: 34px; border-radius: 7px; object-fit: cover;
  background: var(--bg-3); border: 1px solid var(--line); flex: none;
}
.coll .nm { min-width: 0; }
.coll .nm div:first-child { font-weight: 550; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 260px; }

.badge {
  display: inline-block; padding: 2px 8px; border-radius: 5px;
  font-size: 11px; font-weight: 600; white-space: nowrap;
}
/* Tint derived from each theme's own semantic colour, so a badge stays legible
   on a white page and on the orchard's dark forest ground alike. */
.badge.minted_out { background: color-mix(in srgb, var(--good) 16%, transparent);   color: var(--good); }
/* Finished, but inferred rather than proven — the contract publishes no cap, so
   this is read from minting having stopped while secondary sales started.
   Muted against minted_out on purpose: the same outcome, less certainty. */
.badge.mint_ended { background: color-mix(in srgb, var(--good) 10%, transparent);
                    color: color-mix(in srgb, var(--good) 78%, var(--fg)); }
.badge.minting    { background: color-mix(in srgb, var(--accent-2) 16%, transparent); color: var(--accent-2); }
.badge.stalled    { background: var(--bg-3); color: var(--muted); }
.badge.warn       { background: color-mix(in srgb, var(--warn) 16%, transparent);   color: var(--warn); }
.badge.bad        { background: color-mix(in srgb, var(--bad) 16%, transparent);    color: var(--bad); }
.badge.info       { background: color-mix(in srgb, var(--purple) 16%, transparent); color: var(--purple); }

.score { font-weight: 700; font-variant-numeric: tabular-nums; }
.score.hi { color: var(--good); }
.score.mid { color: var(--warn); }
.score.lo { color: var(--dim); }

.bar-mini { height: 4px; background: var(--bg-3); border-radius: 3px; overflow: hidden; margin-top: 4px; width: 90px; display: inline-block; }
.bar-mini i { display: block; height: 100%; background: var(--accent-2); }
.bar-mini.full i { background: #22c55e; }

.empty { padding: 60px 20px; text-align: center; color: var(--dim); }

/* ---------- detail drawer ---------- */

.scrim {
  position: fixed; inset: 0; background: color-mix(in srgb, #000 42%, transparent);
  opacity: 0; pointer-events: none; transition: opacity .18s; z-index: 40;
}
.scrim.on { opacity: 1; pointer-events: auto; }

/*
 * While the collection card is up the BOARD does not scroll.
 *
 * Without this, the first swipe on a phone was a coin toss: a finger landing
 * on the sheet's padding or a non-scrolling child was routed to the page
 * underneath, the board moved behind the scrim, and the card sat still. The
 * lock removes the page from the gesture entirely; overscroll-behavior keeps
 * the sheet's own scroll from chaining back up when it hits its top or
 * bottom. Applied via a class the open/close paths toggle, so nothing
 * changes while no card is open.
 */
body.sheet-open { overflow: hidden; }
aside { overscroll-behavior: contain; }

aside {
  position: fixed; top: 0; right: 0; bottom: 0; width: min(760px, 96vw);
  background: var(--bg); border-left: 1px solid var(--line);
  box-shadow: -8px 0 32px rgba(16, 24, 40, .10);
  transform: translateX(100%);
  /*
   * Hidden while closed, for the same reason .controls is.
   *
   * A closed sheet parked at translateX(100%) is still a 760px box sitting off
   * the right edge, and position:fixed escapes the `overflow-x: clip` on html
   * — clipping ancestors do not contain fixed descendants. It does not widen
   * document.scrollWidth, which is why it looks fine when measured, but a
   * pinch-zoom out on a phone pans to it: the content sits in the left half
   * and the right half is empty. The filter panel was given this treatment
   * after the same bug; the sheet never was.
   *
   * The delay holds it visible for the length of the close animation so it
   * still slides out instead of vanishing, and it keeps the sheet's controls
   * out of the tab order while closed.
   */
  visibility: hidden;
  transition: transform .22s cubic-bezier(.3, .8, .4, 1), visibility 0s linear .22s;
  z-index: 50; overflow-y: auto; overscroll-behavior: contain;
}
aside.on {
  transform: none;
  visibility: visible;
  transition: transform .22s cubic-bezier(.3, .8, .4, 1), visibility 0s;
}

.dhead { padding: 20px; border-bottom: 1px solid var(--line); display: flex; gap: 16px; align-items: flex-start; background: var(--bg-2); }
.dhead img, .dhead .thumb-ph { width: 76px; height: 76px; border-radius: 12px; }
.dhead h2 { margin: 0 0 6px; font-size: 20px; letter-spacing: -.3px; }
.dhead .links { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 9px; font-size: 13px; }
.close { margin-left: auto; background: none; border: 0; color: var(--dim); font-size: 22px; cursor: pointer; line-height: 1; padding: 0 4px; }
.close:hover { color: var(--text); }
/* Match the focus treatment every other control has — a clean accent ring on
 * keyboard focus, and no chunky default UA box on click. This button was the one
 * that was missed, so some browsers drew their own box around the x. */
.close:focus { outline: none; }
.close:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 2px; border-radius: 6px; }

.dsec { padding: 18px 20px; border-bottom: 1px solid var(--line); }
.dsec > h3 {
  margin: 0 0 12px; font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .8px; color: var(--dim);
}

.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }
.kv .k { font-size: 11px; color: var(--dim); text-transform: uppercase; letter-spacing: .5px; }
.kv .v { font-weight: 550; margin-top: 2px; word-break: break-word; }
.kv .v.big { font-size: 17px; }

table.mini { font-size: 13px; }
table.mini th { position: static; padding: 6px 8px; font-size: 10px; }
table.mini td { padding: 6px 8px; border-bottom: 1px solid var(--bg-3); }
table.mini tbody tr { cursor: default; }
table.mini tbody tr:hover { background: none; }

.notefield { display: flex; flex-direction: column; gap: 5px; }
.notefield label { font-size: 11px; color: var(--dim); text-transform: uppercase; letter-spacing: .5px; }
.notefield input, .notefield select, .notefield textarea {
  background: var(--bg); border: 1px solid var(--line-2); border-radius: 7px;
  padding: 7px 9px; outline: none; width: 100%;
}
.notefield textarea { min-height: 76px; resize: vertical; }
.notefield input:focus, .notefield select:focus, .notefield textarea:focus {
  border-color: var(--accent-2); box-shadow: 0 0 0 3px rgba(3, 105, 161, .12);
}

.saverow { display: flex; align-items: center; gap: 10px; margin-top: 14px; }

.scorelist { display: flex; flex-direction: column; gap: 7px; }
.scorerow { display: grid; grid-template-columns: 130px 1fr auto; gap: 10px; align-items: center; font-size: 13px; }
.scorerow .track { height: 6px; background: var(--bg-3); border-radius: 3px; overflow: hidden; }
.scorerow .track i { display: block; height: 100%; background: var(--accent-2); }
.scorerow.neg .track i { background: var(--bad); }
.scorerow .pts { font-variant-numeric: tabular-nums; font-weight: 600; min-width: 62px; text-align: right; }

/* ── public tier profile: a real card, in the app's own language ── */
/*
 * grid-template-columns is LOAD-BEARING, not decoration.
 *
 * This card is a one-column grid, and an implicit `auto` track is sized from
 * its items' MINIMUM contributions — it is never clamped to the container the
 * way a flex item is. .pt-addr carries `white-space: nowrap`, so its
 * min-content is the whole string; that travelled up through .pt-id into the
 * .pt-head flex row (42px roundel + 11px gap + 296px sentence = 349px) and
 * became the width of the single column. Every row in the card — the head, the
 * five-step ladder, the "unlocks" line, the two sign-in links — was then
 * stretched to 349px inside a 267px card and sliced off by the `overflow:
 * hidden` below. That is why copy that has no nowrap of its own still came out
 * chopped mid-word: it was wrapping to a box wider than the card.
 *
 * minmax(0, 1fr) floors the track at 0 so it can only ever be the card's own
 * content width, and the text wraps to what is actually visible.
 */
.pubtier {
  --tierhue: var(--dim);
  display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; padding: 14px;
  background: var(--bg-2); border: 1px solid var(--line); border-radius: 12px;
  max-width: 100%; box-sizing: border-box; overflow: hidden;
}
.pubtier[data-t="seed"] { --tierhue: var(--good); }
.pubtier[data-t="grove"] { --tierhue: var(--accent); }
.pubtier[data-t="orchard"] { --tierhue: var(--warn, #b45309); }
.pubtier[data-t="forest"] { --tierhue: #e8b62e; }
/* Signed in on the public build the profile card comes first and the tier card
   sits under it — two sibling blocks in #authbox, so they need the gap the
   single card never did. Nothing else changes for either one. */
.auth-card + .pubtier { margin-top: 10px; }

.pt-head { display: flex; align-items: center; gap: 11px; min-width: 0; }
.pt-roundel {
  flex: none; width: 42px; height: 42px; border-radius: 50%; font-size: 21px;
  display: grid; place-items: center;
  background: color-mix(in srgb, var(--tierhue) 16%, var(--bg-3));
  border: 1.5px solid color-mix(in srgb, var(--tierhue) 55%, var(--line));
}
.pt-id { flex: 1 1 auto; min-width: 0; }
.pt-name {
  font-family: var(--mono, monospace); font-weight: 800; font-size: 14px;
  letter-spacing: .12em; color: var(--tierhue);
  min-width: 0; overflow-wrap: anywhere;
}
.pt-name small { color: var(--dim); font-weight: 500; letter-spacing: .02em; margin-left: 6px; }
/* A 0x… address has no break opportunities, so it stays on one line and
   ellipsizes — that is the intended look for a wallet. */
.pt-addr {
  color: var(--dim); font-size: 11.5px; font-family: var(--mono, monospace);
  min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/*
 * …but the signed-out slot holds a SENTENCE, not an address, and the address
 * treatment cut it to "Enter your wallet address to u…". Same slot, different
 * kind of content, so app.js tags it and it wraps instead of truncating.
 */
.pt-addr.pt-addr-hint {
  white-space: normal; overflow: visible; text-overflow: clip;
  overflow-wrap: anywhere; line-height: 1.45;
}

/* the ladder: five steps, filled to the current tier, next step breathing */
.pt-ladder { display: flex; align-items: center; gap: 0; }
.pt-step {
  flex: none; width: 26px; height: 26px; border-radius: 50%; font-size: 12.5px;
  display: grid; place-items: center; position: relative;
  background: var(--bg-3); border: 1.5px solid var(--line);
  filter: grayscale(1) opacity(.55);
}
.pt-step.done, .pt-step.now {
  filter: none;
  background: color-mix(in srgb, var(--tierhue) 14%, var(--bg-3));
  border-color: color-mix(in srgb, var(--tierhue) 60%, var(--line));
}
.pt-step.now { box-shadow: 0 0 0 3px color-mix(in srgb, var(--tierhue) 22%, transparent); }
@media (prefers-reduced-motion: no-preference) {
  .pt-step.next { animation: ptBreathe 2.2s ease-in-out infinite; }
  @keyframes ptBreathe { 0%,100% { filter: grayscale(1) opacity(.55); } 50% { filter: grayscale(.4) opacity(.9); } }
}
.pt-rail { flex: 1 1 4px; height: 2px; background: var(--line); min-width: 3px; }
.pt-rail.done { background: color-mix(in srgb, var(--tierhue) 55%, var(--line)); }
.pt-step .pt-req {
  position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
  margin-top: 3px; font-size: 9px; color: var(--dim); font-family: var(--mono, monospace);
  white-space: nowrap;
}
/* 3px of inset keeps the 👀 and 🏆 caps (and their 0 / 100 captions) off the
   card's inner edge — the rails absorb it, so the ladder still spans the card. */
.pt-ladder { padding: 0 3px 16px; }

.pt-next { font-size: 12.5px; color: var(--muted); line-height: 1.5; min-width: 0; overflow-wrap: anywhere; }
.pt-next b { color: var(--text); }

/* same one-column-grid guard as .pubtier — an auto track would take its width
   from the Read button's nowrap min-content rather than from the card. */
.pt-form { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; }
.pt-form[hidden] { display: none; }
.pt-testlink { justify-self: start; border: 0; background: none; padding: 0; cursor: pointer; color: var(--dim); font-size: 11.5px; text-decoration: underline dotted; text-underline-offset: 3px; }
.pt-form label { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--dim); }
/*
 * The address row must be allowed to SHRINK. A flex item's default minimum is
 * its content width, so on a phone the 0x… input kept its natural width, the
 * Continue button kept its padding, and the pair pushed straight through the
 * side of the card. min-width:0 on the row and the form lets the input give way,
 * and border-box stops the padding adding to the width it was given.
 */
.pt-form, .pt-row { min-width: 0; max-width: 100%; box-sizing: border-box; }
.pt-row { display: flex; gap: 8px; flex-wrap: nowrap; align-items: stretch; }
.pt-row > * { box-sizing: border-box; }
.pt-in {
  flex: 1; min-width: 0; padding: 10px 11px; border-radius: 9px; font-family: var(--mono, monospace);
  font-size: 12.5px; border: 1px solid var(--line-2); background: var(--bg); color: var(--text);
}
.pt-in:focus { outline: none; border-color: var(--accent); }
.pt-go {
  /* narrow phones get a tighter button rather than an overflowing row */
  flex: 0 0 auto; padding: 10px 16px; border-radius: 9px; cursor: pointer; font-weight: 700; font-size: 13px;
  border: 1px solid transparent; background: var(--accent-strong, var(--accent)); color: #fff;
  white-space: nowrap;
}
@media (max-width: 420px) { .pt-go { padding: 10px 12px; } .pt-in { font-size: 12px; padding: 10px 9px; } }
.pt-out {
  justify-self: end; padding: 2px 0; border: 0; background: none; cursor: pointer;
  color: var(--dim); font-size: 12px; text-decoration: underline; text-underline-offset: 3px;
}
.pt-out:hover { color: var(--bad); }


/*
 * What each tier does NOT see (server enforces the same — this is the chrome).
 * PHONE-AWARE: the ☰ drawer stays for every tier because it carries things
 * visitors DO get (live-feed switch, theme, language, and the profile box
 * that upgrades them). Only the locked controls inside it disappear.
 */
/* Locked controls GHOST rather than vanish: dimmed, inert, wearing their
   price tag — the visitor sees exactly what a single tree unlocks. */
body[data-pub-tier="visitor"] .ctrl-field[data-label="Search"],
body[data-pub-tier="visitor"] .ctrl-field[data-label="Status"],
body[data-pub-tier="visitor"] #source, body[data-pub-tier="visitor"] #scope,
body[data-pub-tier="visitor"] #social, body[data-pub-tier="visitor"] #quicksort,
body[data-pub-tier="visitor"] #nodata,
body[data-pub-tier="visitor"] .sel[data-label="Show change over"] {
  opacity: .38; pointer-events: none; filter: grayscale(.6); position: relative;
}
/*
 * The lock badge sits ON the control it guards, vertically centred against the
 * control's own box — not floated above it. `top:50%` alone resolved against
 * differing line-heights per control, which left the glyph riding high on some
 * of them; centring the glyph's own height with a flex line-box fixes it for
 * every control the rule touches.
 *
 * #quicksort is deliberately NOT in this list: "⚡ Fastest secondary sales"
 * already reads as one designed unit and a badge crowding its right edge made
 * it look broken rather than gated. The control is still dimmed and inert for
 * visitors, and tapping it explains why.
 */
/* #status is no longer badged here: for a visitor its lock was the only one of
   these that actually rendered (the Search .ctrl-field is display:contents, so
   its ::after never paints), which left a lone padlock floating on the Status
   filter. That single visitor lock now rides the desktop sort hint instead —
   see the .sort-hint::before rule below. */
body[data-pub-tier="visitor"] .ctrl-field[data-label="Search"]::after {
  content: '🔒';
  position: absolute; right: 8px; top: 0; bottom: 0;
  display: flex; align-items: center;
  font-size: 12px; line-height: 1; opacity: 1; filter: none;
  pointer-events: none;
}
/*
 * Any lock that follows a run of TEXT rides the text baseline instead, so it
 * reads as part of the sentence rather than as a superscript floating off the
 * top of it. Used by the desktop sort hint.
 */
.lock-inline {
  font-size: 11px; line-height: 1; margin-left: 5px;
  display: inline-block; vertical-align: baseline; position: relative; top: 1px;
  opacity: .85;
}
/*
 * The visitor's single sort-lock padlock, sitting on the desktop sort hint
 * itself. For a visitor, clicking a column heading does nothing — sorting is
 * locked to last-mint — so the padlock reads as part of that sentence: it sits
 * immediately before "Click any column heading to sort". Placed here (not on a
 * toolbar control) at the Director's request, replacing the lone padlock that
 * floated on the Status filter. Scoped to visitor, and desktop-only for free:
 * .sort-hint is display:none at ≤767px (there are no column headings on mobile),
 * so this ::before never paints there. Holder builds set no pub-tier and never
 * match. Styling mirrors .lock-inline but hugs the text on its right.
 */
body[data-pub-tier="visitor"] .sort-hint::before {
  content: '🔒';
  margin-right: 5px; font-size: 11px; line-height: 1;
  display: inline-block; vertical-align: baseline; position: relative; top: 1px;
  opacity: .85;
}
/* sort control stays visible for visitors; every option but last-mint is disabled in JS */
/* LOCKED, NOT GONE: the icon stays, greyed with a lock badge; tapping explains. */
/* ── public chrome: login pill, view segment, locked card body ── */
.acct-loginmode { width: auto !important; padding: 0 4px !important; }
/*
 * The one call to action on a visitor's screen, so it gets the only piece of
 * dimensional treatment in the header: a soft vertical gradient rather than a
 * flat fill, a hairline of light along the top edge to catch it, and a shadow
 * tinted with the accent instead of neutral grey — the difference between a
 * coloured rectangle and a button that looks pressable. Everything else in
 * this toolbar stays flat by design, which is what lets this one read as the
 * primary action without shouting.
 */
.acct-login-pill {
  display: inline-flex; align-items: center; height: 32px; padding: 0 18px;
  border-radius: 999px; font-weight: 650; font-size: 13px; letter-spacing: .01em;
  color: #fff; white-space: nowrap;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--accent) 92%, #fff) 0%,
    var(--accent-strong, var(--accent)) 100%);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 34%, transparent),
    0 1px 2px color-mix(in srgb, var(--accent-strong, #000) 30%, transparent),
    0 4px 12px -4px color-mix(in srgb, var(--accent-strong, #000) 42%, transparent);
  transition: box-shadow .16s ease, transform .16s ease, filter .16s ease;
}
.acct-loginmode:hover .acct-login-pill {
  filter: brightness(1.05);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 40%, transparent),
    0 2px 4px color-mix(in srgb, var(--accent-strong, #000) 32%, transparent),
    0 8px 20px -6px color-mix(in srgb, var(--accent-strong, #000) 52%, transparent);
}
/* Pressing it should feel like pressing it, not like a page navigation. */
.acct-loginmode:active .acct-login-pill {
  transform: translateY(1px);
  box-shadow:
    inset 0 1px 2px color-mix(in srgb, var(--accent-strong, #000) 45%, transparent),
    0 1px 2px color-mix(in srgb, var(--accent-strong, #000) 25%, transparent);
}
.acct-loginmode:focus-visible .acct-login-pill {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  .acct-login-pill { transition: none; }
  .acct-loginmode:active .acct-login-pill { transform: none; }
}
.pt-loginways { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; }
.pt-way {
  display: block; padding: 11px 13px; border-radius: 10px; text-decoration: none;
  background: var(--bg-3); border: 1px solid var(--line); color: var(--muted);
  font-size: 12.5px; line-height: 1.5;
  min-width: 0; box-sizing: border-box; overflow-wrap: anywhere;
}
.pt-way b { color: var(--text); } .pt-way i { color: var(--accent); font-style: normal; }
.pt-way:hover { border-color: var(--line-2); }

/*
 * THE SIGNED-OUT CARD'S PRIMARY ACTION.
 *
 * A holder does not log in by typing their own address — anyone can type
 * anyone's. They log in by connecting on fruittrees.xyz, which hands the
 * address to /enter?a=…, so THAT is what wears the button. Deliberately not a
 * new style system: the fill, the inner light hairline and the accent-tinted
 * shadow are the same recipe as .acct-login-pill (the header's primary
 * control) laid out on the .auth-login row (icon · two lines · arrow), so the
 * card gains a primary action without gaining a new visual language.
 *
 * .pt-way underneath keeps its quiet card treatment and .pt-testlink stays a
 * dotted whisper — the hierarchy is carried by weight, not by wording alone.
 */
.pt-signin {
  display: flex; align-items: center; gap: 10px; padding: 11px 13px;
  border-radius: 11px; text-decoration: none;
  border: 1px solid transparent;
  /*
   * NOT #fff. --accent-strong is a DARK teal on the light theme and a BRIGHT
   * teal on the dark/orchard ones, so a hardcoded white label is unreadable in
   * half the themes. var(--bg) is the codebase's own answer (see .dl-unlockbar
   * etc.): it is light where the accent is dark and dark where it is bright.
   */
  color: var(--bg);
  /* the card is a minmax(0,1fr) grid — this must be allowed to shrink into it */
  min-width: 0; max-width: 100%; box-sizing: border-box;
  /*
   * Flat --accent-strong, not a gradient. The .acct-login-pill recipe lightens
   * its top stop toward --accent, which on the LIGHT theme is the paler #0d9488:
   * the label then measured 3.33:1 against that stop — below AA for 13.5px, on
   * the one control the card most needs a holder to read. The fill is now the
   * codebase pattern for an accent button (background: var(--accent-strong);
   * color: var(--bg)), and the dimension comes from the inset hairline and the
   * accent-tinted shadow below instead. Measured: 5.5:1 light, 10:1 dark,
   * 6.5:1 orchard.
   */
  background: var(--accent-strong, var(--accent));
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 34%, transparent),
    0 1px 2px color-mix(in srgb, var(--accent-strong, #000) 30%, transparent),
    0 4px 12px -4px color-mix(in srgb, var(--accent-strong, #000) 42%, transparent);
  transition: box-shadow .16s ease, transform .16s ease, filter .16s ease;
}
.pt-signin:hover {
  filter: brightness(1.05);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 40%, transparent),
    0 2px 4px color-mix(in srgb, var(--accent-strong, #000) 32%, transparent),
    0 8px 20px -6px color-mix(in srgb, var(--accent-strong, #000) 52%, transparent);
}
.pt-signin:active {
  transform: translateY(1px);
  box-shadow:
    inset 0 1px 2px color-mix(in srgb, var(--accent-strong, #000) 45%, transparent),
    0 1px 2px color-mix(in srgb, var(--accent-strong, #000) 25%, transparent);
}
.pt-signin:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.pt-signin-ic {
  width: 30px; height: 30px; flex: none; display: grid; place-items: center;
  font-size: 16px; border-radius: 9px;
  background: color-mix(in srgb, var(--bg) 18%, transparent);
}
.pt-signin-tx { display: flex; flex-direction: column; line-height: 1.3; min-width: 0; }
/* 320px: "fruittrees.xyz" has no break opportunity of its own, so let it break */
.pt-signin-tx b { font-size: 13.5px; font-weight: 700; letter-spacing: .01em; overflow-wrap: anywhere; }
.pt-signin-tx i {
  font-size: 11px; font-style: normal; margin-top: 2px;
  color: color-mix(in srgb, var(--bg) 94%, transparent); overflow-wrap: anywhere;
}
.pt-signin-go {
  flex: none; margin-left: auto; padding-left: 6px; font-size: 16px;
  color: color-mix(in srgb, var(--bg) 88%, transparent);
}
@media (max-width: 380px) {
  .pt-signin { padding: 10px 11px; gap: 8px; }
  .pt-signin-ic { width: 26px; height: 26px; font-size: 14px; }
  .pt-signin-tx b { font-size: 13px; }
}
@media (prefers-reduced-motion: reduce) {
  .pt-signin { transition: none; }
  .pt-signin:active { transform: none; }
}

.pub-seg {
  flex: 1 1 auto; min-width: 0; min-height: 40px; display: flex; gap: 0;
  padding: 3px; box-sizing: border-box;
  background: var(--bg-2); border: 1px solid var(--line-2); border-radius: var(--radius);
}
.pub-seg button {
  flex: 1; border: 0; cursor: pointer; border-radius: calc(var(--radius) - 3px);
  background: transparent; color: var(--dim); font-weight: 600; font-size: 13.5px;
  transition: background-color .15s ease, color .15s ease;
}
.pub-seg button.on { background: var(--bg); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,.22); }
.pub-viewbar { position: static; border-bottom: 0; margin-bottom: 0; padding-bottom: 2px; }

/* on public phones the drawer's own view switch retires — the segment owns it */
@media (max-width: 767px) { body[data-pub-tier] #tabs { display: none; } }
@media (min-width: 768px) { .pub-seg { display: none; } }

/* visitor lockdown: real card, analysis greyed in place */
#drawer.tier-lockdown > *:not(.dhead):not(.dl-unlockbar) {
  opacity: .55; pointer-events: none; user-select: none;
  filter: blur(7px) grayscale(.6);
}
.dl-unlockbar {
  display: flex; align-items: center; gap: 12px;
  margin: 12px 20px 4px; padding: 12px 14px; border-radius: var(--radius);
  background: var(--bg-2); border: 1px solid var(--line-2);
}
.dl-ico {
  width: 34px; height: 34px; flex: 0 0 34px; display: grid; place-items: center;
  border-radius: 10px; background: var(--bg-3); border: 1px solid var(--line);
  font-size: 15px;
}
.dl-txt { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 2px; }
.dl-txt b { font-size: 13px; color: var(--text); }
.dl-txt span { font-size: 11.5px; color: var(--dim); line-height: 1.4; }
.dl-txt span b { font-size: inherit; }
.dl-chips { display: flex; flex-direction: column; gap: 4px; align-items: flex-end; }
.dl-chips i {
  font-style: normal; font-size: 11px; white-space: nowrap; padding: 3px 9px;
  border-radius: 999px; background: var(--bg-3); border: 1px solid var(--line);
  color: var(--muted);
}

/* detail header: links and the star/close controls sit on ONE baseline */
#drawer .links { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; }
#drawer .links a { display: inline-flex; align-items: center; height: 26px; }
#drawer .d-head, #drawer .links + * { align-items: center; }

/* "Fetch details" — the on-demand fill button on a genuinely blank card.
   A compact accent button, native to the header, sitting under the links row.
   Adapts per theme (radius/accent/head-font) and reads the same on desktop 1280
   and phone 375. Markup: fetchDetailsBtn(); behaviour: wireDetail() in app.js. */
.cd-fetch {
  display: inline-flex; align-items: center; gap: 7px;
  margin-top: 10px; padding: 7px 13px;
  border: 1px solid var(--accent); border-radius: var(--radius);
  background: var(--bg); color: var(--accent-2);
  font-family: var(--head-font); font-size: 12.5px; font-weight: 650;
  line-height: 1; white-space: nowrap; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background 120ms ease, color 120ms ease, filter 120ms ease, opacity 120ms ease;
}
.cd-fetch:hover:not(:disabled) { background: var(--accent); color: var(--bg); }
.cd-fetch:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 2px; }
.cd-fetch:disabled { cursor: progress; opacity: .8; }
/* deadline passed with nothing landed yet — amber, invites a retry */
.cd-fetch.is-pending { border-color: var(--warn); color: var(--warn); background: var(--bg); }
.cd-fetch.is-pending:hover:not(:disabled) { background: var(--warn); color: var(--bg); }
.cd-fetch .cd-fetch-ico {
  display: inline-grid; place-items: center; width: 13px; height: 13px;
  font-size: 13px; line-height: 1;
}
/* the glyph becomes a spinner ring while the fetch is in flight */
.cd-fetch.is-loading .cd-fetch-ico {
  font-size: 0; box-sizing: border-box;
  border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%;
  animation: cd-fetch-spin .7s linear infinite;
}
@keyframes cd-fetch-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .cd-fetch { transition: none; }
  .cd-fetch.is-loading .cd-fetch-ico { animation: none; }
}

/* the visitor's locked sort pill + the sorts popup */
.sort-lockpill {
  color: var(--dim); text-align: left; cursor: pointer;
  display: flex; align-items: center;
}
.sp-card { max-width: 400px; }
.sp-body { padding: 16px 18px 20px; }
.sp-lead { margin: 0 0 12px; font-size: 13.5px; color: var(--muted); line-height: 1.5; }
.sp-lead b, .sp-foot b { color: var(--text); }
.sp-grid { display: flex; flex-wrap: wrap; gap: 7px; }
.sp-item {
  padding: 5px 11px; border-radius: 999px; font-size: 12px; color: var(--dim);
  background: var(--bg-3); border: 1px solid var(--line);
}
.sp-foot { margin: 14px 0 0; font-size: 12.5px; color: var(--muted); line-height: 1.5; }

.tier-locked { opacity: .4; filter: grayscale(1); position: relative; }
.tier-locked-soft { opacity: .55; }
.tier-locked-soft option:disabled { color: var(--dim); }

/*
 * Visitor score teaser. On the public build a visitor must not read collection
 * scores in the clear, so the score chip is rendered with a heavy blur: the
 * digits are unreadable but the chip still shows, signalling "there is a score
 * here, unlock it". Same lock language as .tier-locked above (grayscale, so the
 * hi/mid/lo colour can't leak the magnitude either) plus the blur. Applied ONLY
 * for visitors, at two sites — the desktop table Score cell (as
 * `.score.score-locked`) and the card's score stat value (as a bare
 * `.score-locked` span inside the stat's <b>) — so it must stand on its own
 * without depending on .score. Non-visitors never get this class and are
 * unchanged. The blur (5px) stays well inside the table cell's 9–10px padding,
 * so `overflow:hidden` on the cell does not clip it.
 */
.score-locked {
  filter: blur(5px) grayscale(1);
  opacity: .6;
  user-select: none;
  -webkit-user-select: none;
  cursor: help;
}

.tier-toast {
  position: fixed; left: 50%; bottom: 76px; transform: translateX(-50%); z-index: 400;
  background: var(--bg-3); color: var(--text); border: 1px solid var(--line-2);
  padding: 10px 16px; border-radius: 10px; font-size: 13px; box-shadow: 0 6px 24px rgba(0,0,0,.35);
  animation: ttIn .18s ease-out;
}
@keyframes ttIn { from { opacity: 0; transform: translate(-50%, 8px); } to { opacity: 1; transform: translate(-50%, 0); } }
/*
 * Visitors SEE the sort arrows, greyed, rather than not knowing sorting exists.
 *
 * These used to be hidden and the header made unclickable. A reader with no
 * arrow and no cursor change has no way to learn the column is sortable at all,
 * so the locked feature was invisible instead of merely locked. Now the arrow is
 * always drawn, dimmed, and the click is allowed through so the tier gate can
 * answer it — see #tierlock in TIER_LOCK_MAP.
 */
body[data-pub-tier="visitor"] thead th .arrow { display: inline; opacity: .3; }
body[data-pub-tier="visitor"] thead th.sortable { cursor: pointer; }
body[data-pub-tier="visitor"] thead th.sortable:hover .arrow { opacity: .55; }

/* the honest lock note in the drawer, injected for visitors */
.tier-locknote {
  margin: 10px 0; padding: 10px 12px; font-size: 12.5px; line-height: 1.5;
  /* No accent rail. The tinted ground is the whole signal — a coloured left
     edge on a note that is already a distinct surface adds a second frame
     saying the same thing. Padding stays symmetric now that nothing eats it. */
  color: var(--muted); background: var(--bg-3);
}

/* ── push-alert onboarding (iOS/Android walk-through) ── */
.po-card { max-width: 420px; }
.po-body { padding: 16px 18px 20px; }
.po-lead { margin: 0 0 14px; color: var(--muted); font-size: 14px; line-height: 1.5; }
.po-lead b, .po-step b { color: var(--text); }
.po-step {
  display: flex; align-items: flex-start; gap: 10px; padding: 10px 0;
  border-top: 1px solid var(--bg-3); font-size: 13.5px; color: var(--muted); line-height: 1.5;
}
.po-n {
  flex: none; width: 22px; height: 22px; border-radius: 50%; font-style: normal;
  display: grid; place-items: center; font-size: 12px; font-weight: 800;
  background: var(--accent); color: var(--bg);
}
.po-ic { flex: none; font-size: 17px; line-height: 1.3; }
.po-foot { margin: 12px 0 0; font-size: 12.5px; color: var(--dim); }
.po-enable { display: block; width: 100%; margin-top: 14px; padding: 11px; font-size: 14px; font-weight: 700; }

/* ── the "?" guidance on public-build alerts ── */
/*
 * ONE affordance, not two.
 *
 * The box below used to say `display: block` and nothing else, and an author
 * `display` beats the UA sheet's `[hidden] { display: none }` — so the help
 * text was permanently open under every row while the "?" sat above it
 * offering to reveal what you were already reading. Two controls, one of them
 * a lie. The `[hidden]` rule here is what makes the attribute actually stick;
 * everything else is the same look, folded away until asked for.
 *
 * The "?" itself rides the label line beside the kind tag (see .bell-hd), so
 * it costs the row no height at all until it is pressed — identical on the
 * phone and on the desktop, because the row anatomy is identical there too.
 */
.bell-help {
  flex: none; width: 18px; height: 18px; border-radius: 50%; padding: 0;
  display: grid; place-items: center; cursor: pointer;
  border: 1px solid var(--line-2); background: var(--bg-3); color: var(--dim);
  font: 700 11px/1 var(--mono, monospace);
}
.bell-help:hover, .bell-help[aria-expanded="true"] { color: var(--accent); border-color: var(--accent); }
.bell-help:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 2px; }
/* Scoped to .bell-tx so it outweighs the generic `.bell-tx span` sentence
   styling it now lives inside. */
.bell-tx .bell-helpbox {
  display: block; margin-top: 5px; padding: 8px 10px; font-size: 12px;
  /* No accent rail: the tinted panel already separates the explanation from
     the alert text above it, and a green left edge inside a row that is
     itself edged read as a nested system. Left padding stays 10px, matching
     the right — the 3px the border occupied is simply given back to the text,
     and the box's own outer edge never moved. */
  color: var(--muted); background: var(--bg-3);
  line-height: 1.5;
  overflow-wrap: anywhere;
}
.bell-tx .bell-helpbox[hidden] { display: none; }
/* Grows downward only — no width, no transform on the row itself. */
@media (prefers-reduced-motion: no-preference) {
  .bell-tx .bell-helpbox { animation: bell-help-in .16s ease-out; }
}
@keyframes bell-help-in {
  from { opacity: 0; transform: translateY(-3px); }
  to   { opacity: 1; transform: none; }
}

/* ── wallet P/L details receipt (the tab inside a wallet's log popup) ── */
.tw-pnl-li {
  display: flex; align-items: baseline; gap: 10px; padding: 8px 12px;
  border-bottom: 1px solid var(--bg-3); cursor: pointer; font-size: 13px;
}
.tw-pnl-li:hover { background: var(--bg-2); }
.tw-pnl-n { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 38%; }
.tw-pnl-m { flex: 1; min-width: 0; color: var(--dim); }
.tw-pnl-li.up .tw-pnl-m b { color: var(--good, #16a34a); }
.tw-pnl-li.down .tw-pnl-m b { color: var(--bad, #dc2626); }
.tw-pnl-li.unk .tw-pnl-m { font-style: italic; }
.tw-pnl-t { color: var(--dim); font-size: 11px; white-space: nowrap; }
.tw-pnl-foot {
  padding: 10px 12px; border-top: 1px solid var(--line-2);
  display: grid; gap: 4px; font-size: 12.5px; color: var(--dim);
}
.tw-pnl-foot b { color: var(--text); }
.tw-pnl-chip { font-weight: 700; }
.tw-pnl-more { display: block; width: calc(100% - 24px); margin: 8px 12px; text-align: center; }

/* the gate's secondary door — quieter than the primary action above it */
.gate-btn-alt { opacity: .75; margin-top: 8px; }
.gate-btn-alt:hover { opacity: 1; }

/* Detail-sheet skeleton: what fills the opened panel for the few hundred ms
   before the profile lands. Reuses the list skeleton's shimmer pieces. */
.d-skel { padding: 18px 20px; }
.d-skel-head { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
.d-skel-head h2 { margin: 0; font-size: 20px; }
.d-skel-head img, .d-skel-head .thumb-ph { width: 44px; height: 44px; border-radius: 10px; }
.d-skel .sk-line, .d-skel .sk-bar { margin: 10px 0; }

/* ---------- live feed ---------- */

.feed { display: flex; flex-direction: column; gap: 1px; }
.feedrow {
  display: flex; align-items: center; gap: 10px; padding: 9px 10px;
  border-bottom: 1px solid var(--bg-3); cursor: pointer;
}
.feedrow:hover { background: var(--bg-2); }
/*
 * Arrival: each new row walks in on its own — slide down a few pixels, fade
 * up, and a brief background wash that hands the eye the newest line, the way
 * a phone stacks notifications one at a time. Insertion is staggered in
 * app.js; this is only the entrance itself. Readers who ask for reduced
 * motion get instant insertion (the class is never applied).
 */
@media (prefers-reduced-motion: no-preference) {
  /*
   * Every mint is its own row (no merging, no counters — the reader asked for
   * the plain tape back). The ANIMATION alone must therefore tell the story
   * when several identical rows stack: app.js slides the whole column down
   * one row height (so the old top line is SEEN moving below), while the new
   * row sweeps in with a short accent wash + left edge that fades — for that
   * half-second the newest line is unmistakable even among five twins.
   */
  .feedrow.arriving { animation: feedIn .38s cubic-bezier(0.32, 0.72, 0, 1); }
  @keyframes feedIn {
    from { opacity: 0; transform: translateY(-6px) scale(.985); }
    to   { opacity: 1; transform: none; }
  }
}
.feedrow img, .feedrow .thumb-ph { width: 28px; height: 28px; border-radius: 6px; }
.feedrow .t { margin-left: auto; color: var(--dim); font-size: 12px; white-space: nowrap; }

.hint { background: color-mix(in srgb, var(--warn) 10%, transparent); border: 1px solid color-mix(in srgb, var(--warn) 30%, transparent); border-radius: 8px; padding: 11px 13px; font-size: 13px; color: var(--text); margin-bottom: 14px; }
.hint b { color: var(--text); }

/*
 * The ▲/▼ change figure under a value.
 *
 * On its own line rather than beside the number: these columns are right
 * aligned and already tight, and appending "▲ 12.4%" after "0.0221 ETH" made
 * the value itself start wrapping. Below it, the eye reads the figure first
 * and the movement second, which is the right order.
 *
 * Tabular numerals so a column of percentages lines up on the decimal point
 * instead of shifting with every poll.
 */
.chg {
  display: block; margin-top: 2px;
  font-size: 10.5px; font-weight: 600; line-height: 1.2;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.chg-up { color: var(--good); }
.chg-dn { color: var(--bad); }

/*
 * A row that changed place flashes and settles back.
 *
 * Tinting the row's own background rather than a border or an outline: the
 * table has hairline separators and anything with geometry would shift the
 * layout for the length of the flash. Colour-mix keeps it a wash over
 * whatever the row's normal ground is, including the selected state.
 *
 * 1.1s and eased out — long enough to catch peripherally on a board that
 * redraws every 30s, gone before it becomes clutter.
 */
/*
 * Strong, held, then faded — and tuned on the dark theme, not the light one.
 *
 * The first version went straight from 26% to transparent over 1.1s. On a
 * white ground that is a clear pulse; on the near-black ground this board
 * actually runs on, a 26% wash is almost the same colour as the row, and it
 * spends most of its life fainter still because it starts decaying
 * immediately. It was firing correctly and could not be seen.
 *
 * Now it opens at 55%, HOLDS for the first third so there is something to
 * catch out of the corner of an eye, and only then fades. A left edge is
 * carried too: colour alone is weak against a dark background, and it also
 * gives the direction to anyone who cannot separate the two hues.
 */
@keyframes rank-up-flash {
  0%   { background: color-mix(in srgb, var(--good) 55%, transparent);
         box-shadow: inset 3px 0 0 var(--good); }
  35%  { background: color-mix(in srgb, var(--good) 45%, transparent);
         box-shadow: inset 3px 0 0 var(--good); }
  100% { background: transparent; box-shadow: inset 3px 0 0 transparent; }
}
@keyframes rank-dn-flash {
  0%   { background: color-mix(in srgb, var(--bad) 55%, transparent);
         box-shadow: inset 3px 0 0 var(--bad); }
  35%  { background: color-mix(in srgb, var(--bad) 45%, transparent);
         box-shadow: inset 3px 0 0 var(--bad); }
  100% { background: transparent; box-shadow: inset 3px 0 0 transparent; }
}
tr.rank-up > td { animation: rank-up-flash 1.6s ease-out; }
tr.rank-dn > td { animation: rank-dn-flash 1.6s ease-out; }
/* The edge marker belongs on the row, not on every cell in it. */
tr.rank-up > td:not(:first-child), tr.rank-dn > td:not(:first-child) {
  animation-name: rank-up-flash-plain;
}
tr.rank-dn > td:not(:first-child) { animation-name: rank-dn-flash-plain; }
@keyframes rank-up-flash-plain {
  0%   { background: color-mix(in srgb, var(--good) 55%, transparent); }
  35%  { background: color-mix(in srgb, var(--good) 45%, transparent); }
  100% { background: transparent; }
}
@keyframes rank-dn-flash-plain {
  0%   { background: color-mix(in srgb, var(--bad) 55%, transparent); }
  35%  { background: color-mix(in srgb, var(--bad) 45%, transparent); }
  100% { background: transparent; }
}
/*
 * The rank flash is NOT disabled under prefers-reduced-motion.
 *
 * It used to be, and that was both wrong and possibly why it never appeared:
 * the setting is reported by more things than the Windows animation toggle —
 * a browser's battery saver or its own accessibility switch will assert it —
 * and one media query was silently turning the whole feature off with nothing
 * to indicate why.
 *
 * It is also the wrong rule to apply. Reduced motion is about movement:
 * parallax, sliding, zooming, things that provoke vestibular symptoms. A
 * background briefly changing colour in place does not move anything. What it
 * does get is a shorter, gentler treatment, so it still reads as a highlight
 * without pulsing at anyone who asked for calm.
 */
@media (prefers-reduced-motion: reduce) {
  tr.rank-up > td, tr.rank-dn > td,
  tr.rank-up > td:not(:first-child), tr.rank-dn > td:not(:first-child) {
    animation-duration: .9s;
    animation-timing-function: linear;
  }
}

/*
 * Odometer digits.
 *
 * Only the digits that actually changed move, and each moves on its own — a
 * value going 4,948 -> 4,950 rolls the last two and leaves "4,9" completely
 * still, which is what makes it readable rather than a blur. A whole-number
 * cross-fade was the alternative and it is the thing that reads as "an
 * animation happened"; this reads as the number itself changing.
 *
 * Each digit is a column of 0-9 inside a one-character window, moved by
 * translateY. Height is set in `em` so it tracks whatever font-size the
 * surrounding cell uses, from the 11px board columns to the 22px card values.
 */
/*
 * The window is a positioning context and the column is pinned to its top.
 *
 * Earlier versions let the column sit as ordinary in-flow content, and it
 * rendered half a cell out of register — "300" reading as "ECC" — even though
 * every measurement said otherwise (window 16.09px, cell 16.09px, column
 * 160.94px, transform -48.28px = exactly three cells). The measurements were
 * right; what they did not capture is that an in-flow block inside an
 * inline-block still answers to the inherited line-height strut and to how the
 * flex parent resolves its baseline, and those shifted the whole column down
 * by a fraction of a line.
 *
 * position:absolute;top:0 removes all of it. The first cell starts exactly at
 * the window's top edge, whatever line-height the surrounding table row uses,
 * and there is no baseline left to negotiate. Verified by asking the browser
 * which <b> is fully inside the window rather than by looking at it.
 */
/*
 * clip-path, NOT overflow:hidden — because of what overflow does to baselines.
 *
 * An inline-block with overflow:hidden is required by CSS to use its bottom
 * MARGIN EDGE as its baseline, ignoring the text inside it. So every digit
 * window sat on a baseline lower than the font's, which pushed the whole strip
 * up by a couple of pixels — and only values that had just changed render as
 * strips, so a number that ticked jumped above the ones beside it that had
 * not. That is the up-and-down in the report, and it is why it moved around:
 * it followed whichever figures happened to change.
 *
 * clip-path clips exactly the same pixels and leaves the baseline alone. The
 * hidden .od-g digit is in normal flow purely to give the box a real text
 * baseline to sit on; the rolling column is painted over it.
 */
.od { font-variant-numeric: tabular-nums; }
.od-d {
  position: relative;
  display: inline-block;
  clip-path: inset(0);
  height: 1.15em; line-height: 1.15em;
  /*
   * A full character wide, not 0.62 of one.
   *
   * This is what was actually wrong the whole time, and it was never the
   * vertical register: overflow:hidden clips both axes, so a window narrower
   * than the glyph cut the right-hand side off every digit. "0" lost its bowl
   * and became "C", "8" became "E", "6" became "E" — while "1" and "7" are
   * narrow enough to survive, which is why only some figures looked broken and
   * why every vertical measurement agreed. 1ch is exactly the advance width of
   * a digit, and tabular-nums guarantees all ten share it.
   */
  /*
   * No fixed width — the hidden in-flow digit sizes the box.
   *
   * This was 1ch, and `ch` is the advance of the PROPORTIONAL zero, while the
   * settled value renders with tabular figures. Where a font's tabular digits
   * are not exactly the width of its proportional zero, every digit box in a
   * rolling strip was off by a fraction, so a number went subtly uneven for
   * the length of the roll and snapped straight again once it settled back to
   * plain text. Letting .od-g define the width means the strip is laid out by
   * the same glyph metrics as the text it replaces, so the two are identical
   * by construction rather than by a unit that happens to agree.
   */
  text-align: center;
  vertical-align: baseline;
  font-variant-numeric: tabular-nums;
}
/* In normal flow and invisible: its only job is to give the box a text
   baseline for the line it sits on. The rolling column paints over it. */
/* font-weight:inherit, or the box is sized by a BOLD digit.
   <b> is bold by default. The column's digits already inherit the weight, but
   this one did not — and since it is the in-flow element, it set the width of
   every digit box: 8.06px against the 7.55px the same digit occupies in the
   settled text. That 0.5px per digit is what made a number go uneven for the
   length of the roll and snap straight when it finished. */
.od-d > .od-g { visibility: hidden; font-weight: inherit; }
/*
 * The column is ten stacked cells and it is moved in TENTHS OF ITSELF, never
 * in em.
 *
 * An em offset has to agree with the browser's own line-box rounding to land
 * on a digit, and it does not: 11px x 1.15 is 12.65px, line boxes round to
 * whole pixels, and the error accumulates down the column until the window
 * shows the bottom of one digit above the top of the next. On screen that
 * turned "300" into "ECC" and "3,333" into "5,333" — every figure on the
 * board subtly wrong in a way that still looked like a number.
 *
 * A percentage translate is resolved against this element's own rendered
 * height, so -10% is exactly one cell of ten whatever the fractional pixels
 * did. The flex column keeps the ten cells exactly equal.
 */
/*
 * No will-change here, deliberately.
 *
 * It promotes the column to its own compositing layer, and a composited layer
 * is not reliably clipped by an ancestor's overflow:hidden — so all ten digits
 * painted at once, stacked, while every geometric measurement still said the
 * right one was in the window. That is the exact signature of the bug that
 * survived three fixes: getBoundingClientRect agreed, the raster did not.
 * These are small, short, infrequent transitions; they do not need the hint.
 */
.od-d i {
  position: absolute; top: 0; left: 0; right: 0;
  display: block; font-style: normal;
  transition: transform .55s cubic-bezier(.22, .68, .32, 1);
}
/* Ten cells, each exactly the height of the window, so -10% of the column is
   precisely one cell however the fractional pixels land. Block, not flex: a
   flex column inside a fixed-height clipping parent is squashed to the
   parent's height, which made 10% a fraction of a digit and blanked the row. */
.od-d i > b { display: block; height: 1.15em; line-height: 1.15em; font-weight: inherit; }
/*
 * Separators are the SAME kind of box as a digit window, and aligned the same.
 *
 * They were plain inline spans next to inline-block windows. An inline-block
 * with overflow:hidden takes its baseline from its bottom margin edge, while
 * ordinary inline text takes its baseline from the font — so inside "3,333"
 * the comma sat on one baseline and the digits on another, and the number
 * visibly stepped up and down across its own width. Giving both the same
 * height, the same line-height and vertical-align:top removes the comparison
 * entirely: every box in the strip is measured from the same top edge.
 */
.od-s { white-space: pre; }
/* Shortened rather than removed, for the same reason as the rank flash above:
   one media query was able to turn the whole thing off invisibly, and a
   half-second roll is not the kind of motion the setting exists to prevent. */
@media (prefers-reduced-motion: reduce) {
  .od-d i { transition-duration: .3s; }
}

/* Column shedding for the board.
 *
 * The old rule was `max-width: 720px`, which the table never reached — below
 * 768px renderCards() takes over and there is no table to hide columns in. So
 * hide-sm was dead code, and every desktop narrower than ~1500px scrolled the
 * whole page sideways instead. See the COLUMNS comment in app.js.
 *
 * Written widest-first so the two rules compose: under 1200px both apply. */
@media (max-width: 1499px) {
  .hide-md { display: none; }
}
@media (max-width: 1199px) {
  .hide-sm { display: none; }
}
@media (max-width: 720px) {
  thead th { top: 150px; }
}

/* Inline "working on it" marker for the minter check, which takes ~30s. */
.spin {
  display: inline-block;
  width: 10px; height: 10px;
  margin-right: 6px;
  border: 2px solid var(--line);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin .8s linear infinite;
  vertical-align: -1px;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spin { animation: none; } }


/* ═══════════════════════════════════════════════════════════════════════
   PHONE LAYOUT  —  under 768px
   The table becomes a stack of cards. Momentum leads: mint progress and
   secondary velocity in front, score demoted to a chip, because a stalled
   drop holding a high score is the exact thing the Hot sort was rewritten
   to stop surfacing.
   Everything here is scoped inside the media query, so the desktop table
   is untouched.
   ═══════════════════════════════════════════════════════════════════════ */

/* Gap, not a shared border — the cards are separate objects now. */
.cds { display: flex; flex-direction: column; gap: 10px; }

/*
 * A card, not a table row wearing a border-bottom.
 *
 * These were transparent with a single hairline underneath, so on a phone the
 * list read as one undifferentiated column and nothing said where one
 * collection ended and the next began. A surface of its own, a border and a
 * radius is what makes each entry an object you can act on — which matters now
 * that each one carries its own controls and can expand.
 */
.cd {
  /* A column, not a two-column grid. The old layout put EVERYTHING in the
     right-hand column, so the progress bar and stats inherited a 58px indent
     under the artwork — a sixth of a phone card spent on empty space. Now
     only the hero row shares a line with the art; the rest runs full width,
     starting where the artwork starts. */
  display: flex; flex-direction: column; gap: 9px;
  padding: 13px;
  cursor: pointer;
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  text-align: left; width: 100%; color: inherit;
  transition: border-color .15s ease, background-color .15s ease;
}
.cd:active { background: var(--bg-3); }
.cd:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 1px; }

/*
 * Expanded. The whole card lifts, not just the chart panel inside it — an
 * accent drawn around the graph alone made the chart look like a separate
 * floating element rather than part of the collection it belongs to.
 */
.cd.is-open {
  border-color: var(--chart-sales);
  background: color-mix(in srgb, var(--chart-sales) 7%, var(--bg-2));
}
@media (prefers-reduced-motion: reduce) { .cd { transition: none; } }

.cd-art, .cd .thumb-ph {
  width: 46px; height: 46px; border-radius: calc(var(--radius) - 2px);
  object-fit: cover; background: var(--bg-3); border: 1px solid var(--line); flex: none;
}
.cd-hero { display: flex; gap: 12px; align-items: center; min-width: 0; }
.cd-id { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 3px; }
.cd-body { min-width: 0; }   /* legacy — the skeleton still uses it */

/*
 * Every control on this row centres on the collection name's FIRST LINE.
 *
 * They previously each grew their own tap target with a hand-picked negative
 * margin — star 44px/-11, the X 44px/-11, the social icons 30px/-6 — so their
 * visual centres landed 2px apart and the X-and-website pair sat noticeably
 * higher than the star and the close button. The row is flex-start aligned on
 * purpose (a name that wraps must stay top-aligned), which meant those
 * mismatched offsets showed.
 *
 * So the offsets are derived instead of guessed: each control declares only
 * how big its tap target must be, and the shared formula sizes it symmetrically
 * around one 20px line box. Every icon then occupies exactly that line box in
 * layout, and every centre lands on the same pixel — whatever tap size it asks
 * for, and whatever we add to this row next.
 */
/* --cd-row-gap separates the name / social pair / action icons across the row;
   --cd-pair-gap is the tight gap used INSIDE an icon pair. Both the 𝕏↔website
   social pair and the star↔✕ pair below read --cd-pair-gap, so they stay equal
   on every platform (see the .cd-hide rule). */
.cd-top { display: flex; align-items: flex-start; gap: var(--cd-row-gap); --icon-line: 20px; --cd-row-gap: 8px; --cd-pair-gap: 2px; }
.cd-top .cd-star, .cd-top .cd-soc, .cd-top .cd-hide {
  box-sizing: content-box;
  height: var(--icon-line);
  min-height: 0;
  padding-block: calc((var(--tap) - var(--icon-line)) / 2);
  margin-block: calc((var(--icon-line) - var(--tap)) / 2);
}
.cd-top .cd-star { --tap: 44px; }
.cd-top .cd-hide { --tap: 44px; }
.cd-top .cd-soc  { --tap: 30px; }
/*
 * Star↔✕ spacing. Per the Director the gap between the star and the ✕ close
 * button must equal the gap between the 𝕏 (Twitter) and 🌐 (website) links in
 * the social pair. Both are driven off ONE token, --cd-pair-gap: the social pair
 * uses it as its flex `gap` (see .cd-social), and here the ✕ is pulled in so the
 * star↔✕ box gap collapses from the row gap down to that same value —
 *   row gap + (pair − row) = pair.
 * Because it is the token, not a fixed pixel, the two pairs stay equal on every
 * platform automatically, including mobile where `html { zoom: .9 }` (≤640px)
 * scales the row gap, the pair gap and this margin together. The result is a
 * small POSITIVE gap (never an overlap), so the star and the ✕ stay independently
 * tappable at their full 44px targets. Structural replacement for an earlier
 * hard-coded -31px that was tuned to one desktop rendering and overlapped on
 * mobile.
 */
.cd-top .cd-hide { margin-left: calc(var(--cd-pair-gap) - var(--cd-row-gap)); }
/* the social pair is a flex parent of its own; let its children carry the box */
.cd-top .cd-social { align-self: flex-start; }
.cd-name {
  font-weight: 620; font-size: 15px; letter-spacing: -.01em; line-height: 1.3;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1;
}
/* 44px tap target, per WCAG — the desktop star is 23px and too small to hit. */
.cd-star {
  background: none; border: 0; cursor: pointer; color: var(--line-2);
  font-size: 19px; line-height: 1; flex: none;
  min-width: 44px; min-height: 44px; margin: -11px 0;
  display: grid; place-items: center; border-radius: 50%;
}
.cd-star.on { color: #f59e0b; }
.cd-star:focus-visible { outline: 2px solid var(--accent-2); outline-offset: -4px; }
/* X and website, tucked between the name and the star. Small, dim until
   touched, and 30px tap targets so they are hittable without crowding. */
.cd-social { display: inline-flex; gap: var(--cd-pair-gap, 2px); flex: none; align-items: center; }
.cd-soc {
  display: grid; place-items: center; min-width: 30px; min-height: 30px;
  margin: -6px 0; font-size: 14px; text-decoration: none; color: var(--dim);
  border-radius: 8px; line-height: 1;
}
.cd-soc:hover, .cd-soc:focus-visible { color: var(--text); background: var(--bg-3); outline: none; }

.cd-status {
  font-family: var(--mono); font-size: 10px; letter-spacing: .08em;
  text-transform: uppercase; margin-top: 1px;
}
.cd-status.out { color: var(--good); }
.cd-status.ing { color: var(--accent-2); }

.cd-bar {
  height: 5px; background: var(--bg-3); border-radius: 999px;
  overflow: hidden; margin-top: 9px;
}
.cd-bar i { display: block; height: 100%; background: var(--accent); border-radius: 999px; }
.cd-bar.done i { background: var(--good); }

.cd-head {
  display: flex; align-items: baseline; gap: 7px; margin-top: 7px;
  font-family: var(--mono); font-size: 11px; color: var(--muted);
  min-width: 0;
}
/* The supply figure is the part that may shrink; the percentage and the
   sales tag are short and must stay whole. Without this the row simply grew
   and the sales count was cut off past the right edge. */
.cd-head > .cd-of { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 0 1 auto; }
.cd-head > .cd-pct, .cd-head > .cd-velo { flex: none; }
.cd-foot { min-width: 0; flex-wrap: wrap; }
.cd-pct {
  font-size: 19px; font-weight: 700; color: var(--text);
  line-height: 1; letter-spacing: -.02em; font-variant-numeric: tabular-nums;
}
.cd-of { color: var(--dim); font-variant-numeric: tabular-nums; }
.cd-velo {
  margin-left: auto; font-size: 12px; font-weight: 700; color: var(--bad);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.cd-velo.quiet { color: var(--dim); font-weight: 400; }

/*
 * The card's spec strip.
 *
 * Four facts, each a dim micro-label above-left of a solid value, separated by
 * space rather than punctuation. Previously all four were the same grey text
 * at the same weight — "score 14 floor $0.19 minting since 1h last mint 45s
 * ago" — which reads as a sentence and has to be decoded word by word instead
 * of scanned. The label carries the meaning once; the value carries the eye.
 */
.cd-foot {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: 3px 14px; margin-top: 9px;
}
.cd-stat {
  display: inline-flex; align-items: baseline; gap: 5px;
  font-family: var(--mono); white-space: nowrap; min-width: 0;
}
.cd-stat i {
  font-style: normal; font-size: 9px; font-weight: 600;
  letter-spacing: .09em; text-transform: uppercase; color: var(--dim);
}
.cd-stat b {
  font-size: 11.5px; font-weight: 700; color: var(--text);
  font-variant-numeric: tabular-nums;
}
/* How old the floor beside it is. Deliberately quieter than the price and not
   coloured: the age is a qualifier on the number, not a warning about it, and
   a red "3h ago" would read as an error when it is simply a fact about how
   often OpenSea can be asked. Reuses --dim, the same weight the labels use. */
.cd-stat em.cd-age {
  font-style: normal; font-size: 9.5px; font-weight: 600;
  color: var(--dim); margin-left: 1px;
}
/* The dated dot on the desktop Floor column — the muted counterpart to
   .live-dot, carrying its age in the tooltip since the column has no room. */
.floor-age {
  font-style: normal; margin-left: 5px;
  color: var(--dim); cursor: help;
}
/* Score keeps the same three bands as the desktop column, so a glance at the
   colour says as much as reading the number. */
.cd-stat.sc-hi  b { color: var(--good); }
.cd-stat.sc-mid b { color: var(--warn); }
.cd-stat.sc-lo  b { color: var(--muted); }
.cd-hidden { padding: 12px 4px; font-size: 12px; color: var(--dim); }

/* The way back from a hide. Sits above the list so it is reachable on a phone
   without scrolling past every row, and reads as an active offer rather than a
   dim footnote — it is the only undo once the message box has been replaced. */
.hidden-bar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  /* Top margin, not just bottom: on desktop this lands directly under the
     filter bar and without it the two borders sit flush and read as one
     broken control. */
  margin: 12px 0 10px; padding: 8px 12px; border-radius: 8px;
  font-size: 12.5px; color: var(--fg);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent);
}
.hidden-bar a { font-weight: 600; text-decoration: underline; }

/* Respect the OS setting on the sheet and the live dot, not just the spinner. */
@media (prefers-reduced-motion: reduce) {
  aside, .scrim { transition: none !important; }
  .dot.live { animation: none !important; }
}

/* Live feed. This lives here, ABOVE the breakpoint, because the phone rule
   that stacks it has the same specificity — declared afterwards it would win
   everywhere. */
/*
 * ONE column, because there is one tape. It ran three side by side — mints,
 * milestones and the live listing tape — and the last two have been removed
 * from the feed. Leaving `repeat(3, 1fr)` behind would have parked the mints
 * in the left third and left two thirds of the page blank, which is exactly
 * the hole the removal was supposed to close. The mint rows themselves are
 * untouched: same markup, same classes, same arrival animation — this only
 * says how many columns the grid has, and a single full-width feed column is
 * already what every reader under 720px has always seen.
 */
.feedgrid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 24px; align-items: start; }

/*
 * The arrival highlight. One pass of light that peaks as the row settles and
 * is gone a beat later — enough to catch the eye mid-scan, not enough to leave
 * the tape looking decorated. Colour comes from the accent so it belongs to
 * the product rather than reading as a generic "new" badge, and it rides a
 * pseudo-element so it can never disturb the row's own layout or hit-testing.
 */
.feedrow { position: relative; }
.feed-arrive::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--accent) 16%, transparent) 0%,
    color-mix(in srgb, var(--accent) 6%, transparent) 45%,
    transparent 78%);
  animation: feedArrive 900ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes feedArrive {
  0%   { opacity: 0; }
  22%  { opacity: 1; }
  100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .feed-arrive::after { animation: none; opacity: 0; }
}

/* The panel's theme switcher is phone-only; the header keeps its own. Same
   ordering trap: this must precede the breakpoint that reveals it. */
.ctrl-theme { display: none; }

/* ── hamburger (phone only; hidden on desktop below) ────────────────────── */
.hamb {
  display: none; position: relative;
  width: 44px; height: 44px; flex: none;
  background: none; border: 1px solid var(--line); border-radius: var(--radius);
  cursor: pointer; padding: 0;
  flex-direction: column; align-items: center; justify-content: center; gap: 4px;
}
.hamb span { display: block; width: 17px; height: 2px; background: var(--text); border-radius: 2px; }
.hamb:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 2px; }
.hamb-n[hidden] { display: none; }
.hamb-n {
  position: absolute; top: -5px; right: -5px;
  background: var(--accent); color: var(--bg);
  font-family: var(--mono); font-size: 10px; font-weight: 700; font-style: normal;
  min-width: 17px; height: 17px; border-radius: 999px;
  display: grid; place-items: center; padding: 0 4px;
}
.ctrl-head, .ctrl-close { display: none; }
.ctrl-scrim { display: none; }

/* ═══════════════════════════════════════════════════════════════════════
   HOLDER GATE  —  /login
   One card, centred, no chrome. Sized in clamp() and vw so it needs no
   breakpoint of its own; it must sit ABOVE the phone media query below,
   or those rules could not override it at equal specificity.
   ═══════════════════════════════════════════════════════════════════════ */
.gate-body {
  min-height: 100dvh;              /* dvh, so mobile browser chrome cannot clip it */
  display: grid; place-items: center;
  padding: 24px 20px;
  background: var(--bg);
}
.gate {
  width: 100%; max-width: 380px;
  text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
}
.gate-mark { width: 64px; height: 64px; display: block; margin: 0 auto; }
.gate h1 {
  margin: 0;
  font-family: var(--head-font);
  font-size: clamp(20px, 5.5vw, 25px);
  font-weight: 700; letter-spacing: -0.01em;
  text-wrap: balance;
  color: var(--text);
}
.gate-sub {
  margin: -6px 0 4px;
  font-size: 13.5px; line-height: 1.55;
  color: var(--muted);
  text-wrap: pretty;
}
.gate-btn {
  width: 100%;
  /* 48px: the minimum comfortable touch target, and this is the only control
     on the page — nothing here is worth making anyone aim for. */
  min-height: 48px;
  /* Centres the label whether this is a <button> or an <a>. It is a link now:
     the gate sends people to the main site to connect rather than asking for a
     wallet here, so there is nothing on this page to click that acts. */
  display: inline-flex; align-items: center; justify-content: center;
  text-decoration: none;
  padding: 0 20px;
  border: 1px solid transparent; border-radius: var(--radius);
  background: var(--accent-strong); color: var(--bg);
  font-family: var(--head-font); font-size: 15px; font-weight: 650;
  cursor: pointer;
  transition: filter 120ms ease, opacity 120ms ease;
}
.gate-btn:hover:not(:disabled) { filter: brightness(1.08); }
.gate-btn:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 3px; }
.gate-btn:disabled { opacity: 0.55; cursor: progress; }

.gate-msg {
  margin: 0; min-height: 2.6em;    /* reserved, so the card does not jump */
  font-size: 13px; line-height: 1.5;
  color: var(--muted);
  text-wrap: pretty;
}
.gate-msg.is-bad  { color: var(--bad); }
.gate-msg.is-good { color: var(--good); }

.gate-foot {
  margin: 2px 0 0;
  padding-top: 14px;
  border-top: 1px solid var(--line);
  width: 100%;
  font-size: 11.5px; line-height: 1.55;
  color: var(--dim);
  text-wrap: pretty;
}

@media (prefers-reduced-motion: reduce) {
  .gate-btn { transition: none; }
}

/* ═══════════════════════════════════════════════════════════════════════
   PHONE  —  under 768px
   ═══════════════════════════════════════════════════════════════════════ */
@media (max-width: 767px) {

  /* ── header: one sticky block, brand row then a separated pills row ──── */
  header {
    position: sticky; top: 0;
    /*
     * backdrop-filter makes an element a CONTAINING BLOCK for any
     * position:fixed descendant. The filter panel lives inside the header, so
     * "fixed to the viewport" was really "fixed to the header box" — a strip a
     * few rows tall — and the panel opened inside it, behind the cards and
     * effectively invisible. The blur is decoration; the panel is not.
     */
    backdrop-filter: none;
    background: var(--bg);
  }
  thead th { position: static; }

  .bar {
    padding: 9px 14px 0; gap: 10px; flex-wrap: wrap;
    border-bottom: 0;                     /* the pills row carries the rule */
    /*
     * The pills row bleeds past this padding with a negative margin so its
     * separator reaches both edges. Clipping it here keeps that bleed from
     * widening the page — and .bar is NOT an ancestor of the fixed filter
     * panel, so unlike clipping the header this cannot hide it.
     */
    overflow-x: clip;
  }
  /*
   * The first header row must FIT, on every width, or the icon cluster wraps
   * under the logo and left-aligns — seen on Opera Mobile, whose 360px
   * viewport is 33px narrower than the Pixel-width phones this was tested on.
   * The row's minimum was rigid: nowrap brand name, desktop gaps and padding.
   * Three changes make it elastic instead: the name may ellipsize (min-width:0
   * down the chain is what allows shrinking below content size), the gaps and
   * padding drop to phone scale, and the cluster pins right with margin-left:
   * auto. The pills strip keeps its own wrapped row via flex-basis:100%.
   */
  /*
   * The phone size the user actually wants is the one Opera's text inflation
   * happened to produce — everything ~15% larger than the raw stylesheet.
   * With inflation now locked to 100% (so layout is deterministic on every
   * browser), that size is applied ON PURPOSE, to everything, uniformly:
   * Safari and Opera now render identically, at the bigger scale.
   */
  body { zoom: 1.15; }
  /*
   * viewport-fit=cover lets the page extend under the phone's status bar and
   * any camera notch — without this inset the top of the header (the logo's
   * upper half, exactly as reported) sits underneath the browser chrome.
   */
  .bar { gap: 8px; padding: 10px 12px; padding-top: max(10px, env(safe-area-inset-top)); }
  .brand { font-size: 14px; flex: 1 1 0; min-width: 0; gap: 8px; }
  /* .brand-name now carries its own size, so the parent's 14px no longer
     reaches it. 15px and a 28px mark is what the main site drops to here. */
  .brand-name { font-size: 15px; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .brand-logo { width: 28px; height: 28px; }
  .brand small { display: none; }
  .barend { flex: none; display: flex; align-items: center; gap: 6px; margin-left: auto; }
  /*
   * A finger needs ~40px; the bell was 26 (18px icon + 3px padding) — the
   * wallet tray was genuinely hard to open on Safari. Grow the BUTTON, not
   * the artwork: same 18px icons, same visual weight, four times the
   * touchable area. The hamburger is already 44px and stays.
   */
  /*
   * THE half-logo bug, measured at 360px: langSel(59) + 4 controls at 44 +
   * gaps = ~259 CSS px of a 313px row (360 ÷ the 1.15 zoom) — the brand was
   * squeezed to 22px and the 28px logo clipped. Every control drops to
   * 40px (× 1.15 zoom = 46 real px, still a full fingertip) and the picker
   * collapses to its two-letter code; the row finally has room for the mark.
   */
  .bell, .hamb { box-sizing: border-box; width: 40px; height: 40px; padding: 0; }
  .hamb { display: flex; }
  /*
   * The language picker is not in the bar on a phone at all — i18n.js re-homes
   * it into the filters panel as a labelled full-width row. The bar row now
   * holds the logo, the name and four 40px buttons, and nothing else.
   */
  .ctrl-lang select#langSel {
    width: 100%; height: 40px; font-size: 14px;
    padding: 0 34px 0 12px; background-position: right 12px center;
    /* The desktop bar pill centers its two-letter code; HERE the picker is a
       full-width labelled form field, where a centered value floats oddly in
       the middle of the row — form fields read left-aligned. */
    text-align: left; text-align-last: left;
  }
  /* Theme is a set-once preference — it belongs in the panel, not the bar. */
  .bar .tabs.theme { display: none; }

  /*
   * The status pills were overflowing off the right edge. They now sit on
   * their own full-width row under a separator and WRAP, so every one is
   * reachable without a sideways scroll nobody knows is there.
   */
  /*
   * Status pills: one swipeable strip, edge to edge.
   *
   * Wrapping them stacked the header three rows deep and pushed the list down
   * the screen. This keeps them on a single line you drag sideways, with no
   * scrollbar drawn — the last pill is deliberately allowed to sit half off the
   * edge, because a clean cut at the boundary reads as "that is all of them"
   * and hides the fact that there is more to see.
   */
  .pills {
    /*
     * Span the full bar, both edges.
     *
     * flex-basis:100% resolves against the bar's CONTENT box (375 - 28 padding
     * = 347), so the negative left margin pulled the row to x=0 but its width
     * stayed 347 and the right edge stopped 28px short — the strip of bare
     * background down the right of the header.
     *
     * It has to be set on flex-basis, not width: for a flex item the basis
     * determines the main size and width is ignored, which is why setting
     * width alone changed nothing.
     */
    order: 3; flex-basis: calc(100% + 28px); max-width: none;
    margin: 9px -14px 0; padding: 8px 14px 9px;
    border-top: 1px solid var(--line);
    background: var(--bg-2);
    display: flex; flex-wrap: nowrap; gap: 6px;
    overflow-x: auto; overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    scrollbar-width: none;              /* Firefox */
    -ms-overflow-style: none;           /* old Edge */
    scroll-snap-type: x proximity;
  }
  .pills::-webkit-scrollbar { display: none; }   /* Safari, Chrome */
  /*
   * Fade the trailing edge instead of chopping a number in half. A hard cut
   * through "mints 610" reads as a layout fault; a fade reads as "there is
   * more this way". The mask only bites once the strip actually overflows,
   * because a fully visible row has nothing under the gradient.
   */
  .pills {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
    mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
  }
  .pill {
    flex: none; font-size: 11px; padding: 3px 9px;
    scroll-snap-align: start;
  }

  /* ── filters: off-canvas panel ───────────────────────────────────────── */
  .ctrl-scrim {
    display: block; position: fixed; inset: 0; z-index: 45;
    background: color-mix(in srgb, #000 42%, transparent);
  }
  .ctrl-scrim[hidden] { display: none; }

  .controls {
    position: fixed; top: 0; right: 0; bottom: 0; z-index: 46;
    width: min(340px, 88vw);
    /*
     * nowrap is load-bearing.
     *
     * The desktop rule sets flex-wrap:wrap, and a wrapping COLUMN flexbox with
     * a fixed height does not scroll when it runs out of room — it starts a
     * second column. The panel is 812px tall holding ~1800px of controls, so
     * everything past Status was laid out off to the right: scrollWidth 658
     * against a 329 panel, exactly double. OpenSea data and Export CSV were
     * not below the fold, they were sideways.
     */
    display: flex; flex-direction: column; flex-wrap: nowrap; align-items: stretch;
    gap: 0; padding: 0 0 env(safe-area-inset-bottom, 0);
    background: var(--bg); border-left: 1px solid var(--line);
    box-shadow: -8px 0 32px rgba(0, 0, 0, .28);
    transform: translateX(100%);
    /*
     * Hidden, not merely pushed aside.
     *
     * A closed panel parked at translateX(100%) still occupies its box in the
     * scrollable area: measured at 375px viewport the document was 829px wide,
     * a 454px overhang caused entirely by this element. That is the dead strip
     * down the right of the page and why the header and cards looked cut —
     * the page was simply wider than the window.
     *
     * visibility also keeps its controls out of the tab order while closed,
     * which was wrong before too. The delay holds it visible for the length of
     * the close animation so it slides out rather than vanishing.
     */
    visibility: hidden;
    transition: transform .22s cubic-bezier(.3, .8, .4, 1), visibility 0s linear .22s;
    overflow-y: auto; overscroll-behavior: contain;
  }
  body.filters-open .controls {
    transform: none;
    visibility: visible;
    transition: transform .22s cubic-bezier(.3, .8, .4, 1), visibility 0s;
  }

  /*
   * Sideways-pan containment, UNCONDITIONALLY.
   *
   * The full iOS recipe used to sit inside `@supports not (overflow: clip)`,
   * which a modern iPhone never enters (Safari 16+ knows clip) — so the one
   * phone that actually had the drag-to-an-empty-half bug was the one phone
   * the fallback skipped. It measured perfectly in a Chromium viewport, which
   * is exactly why it survived several passes.
   *
   * Both roots hidden plus position:relative on body is what WebKit honours
   * for horizontal PANNING; the html rule right after upgrades hidden to clip
   * where clip is supported (clip does not create a scroll container, so
   * position:sticky keeps working), and older engines drop that line and keep
   * hidden. overscroll-behavior stops the rubber-band pan that shows the same
   * empty half with nothing actually overflowing. The other half of the fix
   * is minimum-scale=1 in the viewport meta: a page left below 1x by a pinch
   * out pans sideways regardless of every rule here.
   */
  html, body { overflow-x: hidden; overscroll-behavior-x: none; }
  html { overflow-x: clip; }
  body { position: relative; }

  .ctrl-head {
    display: flex; align-items: center; justify-content: space-between;
    padding: 14px 16px; border-bottom: 1px solid var(--line);
    font-family: var(--mono); font-size: 12px; letter-spacing: .1em;
    text-transform: uppercase; color: var(--dim);
    position: sticky; top: 0; background: var(--bg); z-index: 1;
  }
  .ctrl-close {
    display: grid; place-items: center;
    min-width: 44px; min-height: 44px; margin: -10px -12px -10px 0;
    background: none; border: 0; color: var(--muted); font-size: 17px; cursor: pointer;
  }

  /*
   * Every group is a full-width block of equal-height buttons. Previously the
   * groups were sized by their own text, so a two-word group sat next to a
   * five-word one and the row read as broken rather than deliberate.
   */
  .controls > * { margin: 0; }
  /* The sign-in card is the first block under the sticky header; without its
     own inset it ran full-bleed to the panel edges while every control below
     it sat at 16px. Match that gutter and give it breathing room up top. */
  .controls .authbox { margin: 14px 16px 2px; width: calc(100% - 32px); }
  .controls .tabs,
  .controls input[type=search],
  .controls select {
    width: 100%; margin: 0 16px; width: calc(100% - 32px);
  }
  .controls .tabs {
    display: flex; flex-wrap: nowrap; margin-top: 8px; margin-bottom: 16px;
    background: var(--bg-3); overflow: visible;
  }
  .controls .tabs button {
    flex: 1 1 0; min-width: 0; min-height: 42px; padding: 6px 8px;
    font-size: 12.5px; text-align: center; white-space: normal; line-height: 1.25;
  }
  /* The group's purpose was only in a title attribute, which a touch screen
     can never show. On a phone the label has to be on the page. */
  .controls .tabs[data-label]::before {
    content: attr(data-label);
    position: absolute; margin-top: -21px;
    font-family: var(--mono); font-size: 10px; letter-spacing: .1em;
    text-transform: uppercase; color: var(--dim);
  }
  .controls .tabs[data-label] { position: relative; margin-top: 26px; }

  /*
   * The search box and the status select were the only two controls without a
   * label, and with no gap between them they read as one joined element rather
   * than two separate filters. Wrapped and labelled they match View, Show,
   * Money in and the rest, and the shared spacing rule keeps them apart.
   */
  .controls .ctrl-field {
    position: relative;
    width: calc(100% - 32px); margin: 26px 16px 0;
  }
  .controls .ctrl-field:last-of-type { margin-bottom: 4px; }
  .controls .ctrl-field[data-label]::before {
    content: attr(data-label);
    position: absolute; top: -15px; left: 0;
    font-family: var(--mono); font-size: 10px; letter-spacing: .1em;
    text-transform: uppercase; color: var(--dim);
  }
  .controls .ctrl-field input[type=search],
  .controls .ctrl-field select {
    width: 100%; margin: 0; min-height: 44px;
  }

  /* Theme switcher gets a home inside the panel. The header copy is hidden at
     this width, so without this there was no way to change theme on a phone. */
  .ctrl-theme { display: flex; }
  .ctrl-theme button { font-size: 13px; }

  /* A grab handle: the sheet can be swiped down to dismiss, and nothing on
     screen said so. */
  /* The sheet's grab bar. Sits on a band of its own colour so the rounded top
     edge reads as a separate surface lifted over the list, rather than the
     list itself having changed. */
  aside::before {
    content: ''; position: sticky; top: 0; z-index: 3;
    display: block; width: 38px; height: 4px; margin: 0 auto;
    padding: 10px 0; background-clip: content-box;
    box-sizing: content-box;
    border-radius: 999px; background-color: var(--line-2);
  }
  /*
   * pan-y stops the SHEET being dragged sideways. It also, unavoidably,
   * forbids horizontal panning for every touch that starts inside it — which
   * silently disabled the .tscroll wrappers below: the phases table and the
   * minter addresses had real overflow and an overflow-x:auto container, and
   * a finger simply could not move them. The exemption is on the scrollers
   * only, so the sheet itself still cannot be swiped off-axis.
   */
  aside { touch-action: pan-y; }
  aside .tscroll { touch-action: pan-x pan-y; }

  /* Feed stacks — side-by-side columns on a 400px screen mean the second one
     is simply off the edge, which is what pushed the page sideways past the
     header. */
  .feedgrid { grid-template-columns: 1fr; gap: 20px; }
  .feed { max-width: 100%; }
  .feedrow { min-width: 0; }
  .feedrow > div { min-width: 0; }
  .feedrow .mono { overflow-wrap: anywhere; }

  /* Header pills row must reach the right edge as well as the left. */
  .pills { margin-right: -14px; }

  /* ── list ────────────────────────────────────────────────────────────── */
  main, .wrap { padding-left: 14px; padding-right: 14px; }
  /* Cards carried their own 4px inset, so card text began 18px from the edge
     while the header began at 14px — the misalignment read as the header not
     reaching the left. The page padding is the only inset now. */
  /* Keep the card padding on a phone — it has a real border now, and zeroing
     the sides pressed the artwork and text against it. */
  .cd { padding: 12px; }
  .cd-hidden { padding-left: 0; }

  /* ── detail sheet ────────────────────────────────────────────────────── */
  aside {
    top: auto; left: 0; right: 0; bottom: 0; width: 100%;
    max-height: 92dvh; border-left: 0; border-top: 1px solid var(--line);
    border-radius: 16px 16px 0 0;
    transform: translateY(100%);
    overflow-x: hidden;                  /* nothing inside may push it sideways */
    padding-bottom: env(safe-area-inset-bottom, 0);
  }
  aside.on { transform: none; }

  /*
   * A grab handle across the top of the sheet.
   *
   * The panel slid up flush against the cards with nothing but a hairline
   * between them, so there was no edge to read it against and it looked like
   * the page had simply changed. Every native bottom sheet has this bar, and
   * it is the thing that says "this is a layer, and it can be dragged away".
   */
  /*
   * The grab bar already exists further up; it just had no room. It sat 8px
   * from the top edge with the header immediately under it, so the sheet
   * arrived with its content jammed against the rounded corner and read as a
   * page that had changed rather than a panel that had risen over one.
   *
   * Spacing only — the bar itself is defined once, above.
   */
  .dhead { padding: 14px 16px 16px; border-radius: 0; flex-wrap: wrap; }
  .dhead img, .dhead .thumb-ph { width: 56px; height: 56px; }
  .dhead h2 { font-size: 17px; }
  .close { min-width: 44px; min-height: 44px; }
  .dsec { padding: 15px 16px; }
  .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .scorerow { grid-template-columns: 1fr auto; gap: 6px 10px; }
  .scorerow .track { grid-column: 1 / -1; }
}

/* ═══════════════════════════════════════════════════════════════════════
   WIDE CONTENT INSIDE THE DETAIL PANEL
   The phases table is 8 columns and the minters table carries 20-character
   addresses. Neither had an overflow container, so on a phone they pushed
   the whole sheet sideways and half the panel sat off-screen. Each wide
   thing now scrolls inside itself and the sheet never moves.
   ═══════════════════════════════════════════════════════════════════════ */
/*
 * Scroll the WRAPPER, never the table.
 *
 * The first attempt set display:block on the table with thead and tbody each
 * display:table. That makes them two independent tables with independent
 * column widths, so the header row and the body rows stopped lining up — the
 * Length and Take columns had values but they no longer sat under their
 * headings. A wrapper keeps one column model for the whole table.
 */
/*
 * Scrollable, and visibly so.
 *
 * The mechanics were already right — 466px of table inside a 343px box, with
 * pan-x allowed — but iOS hides scrollbars until a scroll is already under
 * way, so the phases table looked like a truncated table rather than a
 * scrollable one and nobody knew to swipe. Two cues fix that:
 *
 * 1. A real slider. scrollbar-width:thin plus a styled WebKit thumb, so there
 *    is something to see and to drag rather than an invisible gesture.
 * 2. The classic four-gradient shadow. background-attachment mixes `local`
 *    (scrolls with content) and `scroll` (pinned to the box), so the shadow
 *    appears at whichever edge has more content behind it and disappears when
 *    you reach the end — pure CSS, no scroll listener, correct at every
 *    position including after a resize.
 */
.tscroll {
  width: 100%; max-width: 100%;
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--line-2) transparent;
  background:
    linear-gradient(to right, var(--bg-2) 30%, transparent) left center,
    linear-gradient(to left, var(--bg-2) 30%, transparent) right center,
    radial-gradient(farthest-side at 0 50%, rgba(0, 0, 0, .22), transparent) left center,
    radial-gradient(farthest-side at 100% 50%, rgba(0, 0, 0, .22), transparent) right center;
  background-repeat: no-repeat;
  background-size: 28px 100%, 28px 100%, 12px 100%, 12px 100%;
  background-attachment: local, local, scroll, scroll;
}
.tscroll::-webkit-scrollbar { height: 8px; }
.tscroll::-webkit-scrollbar-track { background: transparent; }
.tscroll::-webkit-scrollbar-thumb {
  background: var(--line-2); border-radius: 999px;
}
.tscroll::-webkit-scrollbar-thumb:hover { background: var(--dim); }
aside table.mini { width: 100%; min-width: max-content; border-collapse: collapse; }
aside table.mini th, aside table.mini td { white-space: nowrap; }
/*
 * table-layout AUTO here, overriding the global fixed.
 *
 * The global `table { table-layout: fixed }` is a performance fix for the
 * BOARD — thousands of rows re-laid-out per poll. These are eight-row tables,
 * and fixed layout is what broke them on the phone: WebKit does not honour
 * min-width:max-content on a fixed-layout table, so on iOS (Safari and the
 * MetaMask in-app browser alike) the table squeezed to screen width, the
 * nowrap headings overlapped each other, and the cells ellipsized —
 * "MAX / WALAVG / WALLENGTH" and "$135…" in one screenshot. Auto layout is
 * the guarantee fixed cannot give: a table can never shrink below its
 * content, so with nowrap cells it grows to its natural width and the
 * .tscroll wrapper scrolls it, on every engine.
 */
aside .tscroll table.mini { table-layout: auto; }

/* Tighter on a phone so eight columns need less swiping to read. The table
   still scrolls; this just means most of it is visible before you start. */
@media (max-width: 767px) {
  aside table.mini { font-size: 12px; }
  aside table.mini th { padding: 5px 6px; font-size: 9.5px; letter-spacing: .2px; }
  aside table.mini td { padding: 5px 6px; }
  /* Room for the slider under the last row. */
  aside .tscroll { padding-bottom: 2px; }
}
aside .mono { overflow-wrap: anywhere; }
aside .kv .v { overflow-wrap: anywhere; }
aside .dsec { min-width: 0; }
aside .grid { min-width: 0; }
aside .grid > * { min-width: 0; }

/* Respect the OS setting on the sheet and the live dot, not just the spinner. */

/* While the filter panel is open the header must outrank the detail drawer,
   because the panel is nested inside the header's stacking context and its
   own z-index cannot escape it. */
body.filters-open header { z-index: 60; }

/* ── mobile sort bar ────────────────────────────────────────────────────
   The phone has no column headings, so this is the only sort control there
   is. It sticks under the header rather than scrolling away, because
   reordering is something you reach for repeatedly while scanning a list.
   Rendered only inside the card layout, so desktop never sees it. */
/*
 * The sort row shares the card grid.
 *
 * Its label sits in the same 46px column the artwork occupies and the control
 * starts on the same 72px line every card's text starts on, so one edge runs
 * down the page instead of three. Before, the label began at 14, the select at
 * 49 and the card text at 72 — three different left edges within 60px.
 */
.sortbar {
  display: flex; align-items: center; gap: 12px;
  padding: 8px 0; margin-bottom: 6px;
  border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 5;
  background: var(--bg);
}
.sortbar-l {
  font-family: var(--mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--dim);
  flex: none; width: 46px;      /* the artwork column */
}
/* The Fast Mints window selector, sitting snug under the sort row. Not sticky
   (only the sort row pins), no top border, so the two read as one control. Its
   select is full width — the same knob, just its own line. */
.sortbar-sub {
  position: static; border-bottom: 1px solid var(--line); border-top: 0;
  /* Pulled flush under the sort row (−6px cancels its bottom margin) so the
     selector sits between the two divider lines with EQUAL 8px above and below
     — not shoved toward the bottom as it was. */
  margin-top: -6px; margin-bottom: 0; padding: 8px 0; z-index: 4;
}
.sortbar-sel {
  flex: 1 1 auto; min-width: 0; min-height: 40px;
  background: var(--bg-2); color: var(--text);
  border: 1px solid var(--line-2); border-radius: var(--radius);
  padding: 6px 10px; font-size: 14px;
}
.sortbar-dir {
  flex: none; min-height: 40px; padding: 0 12px;
  background: var(--bg-2); color: var(--muted); cursor: pointer;
  border: 1px solid var(--line-2); border-radius: var(--radius);
  font-family: var(--mono); font-size: 11.5px; white-space: nowrap;
}
.sortbar-dir:active { background: var(--bg-3); color: var(--text); }
.sortbar-sel:focus-visible, .sortbar-dir:focus-visible {
  outline: 2px solid var(--accent-2); outline-offset: 1px;
}


/* ── collapsible feed sections ──────────────────────────────────────────
   Stacked on a phone the second column starts far down the page, so folding
   the first is what makes it reachable. <details> gives the state and the
   keyboard handling; only the marker needs styling. */
.feedsec { min-width: 0; }
.feedsec > summary {
  list-style: none; cursor: pointer;
  display: flex; align-items: center; gap: 8px;
  padding: 10px 0; margin: 0;
  font-size: 12px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .8px; color: var(--muted);
  border-bottom: 1px solid var(--line);
  min-height: 44px;
}
.feedsec > summary::-webkit-details-marker { display: none; }
.feedsec > summary::before {
  content: '▸'; font-size: 11px; color: var(--dim);
  transition: transform .15s; display: inline-block;
}
.feedsec[open] > summary::before { transform: rotate(90deg); }
.feedsec > summary:hover { color: var(--text); }
.feedsec > summary:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .feedsec > summary::before { transition: none; }
}

/* ── card: supply flag + floor ──────────────────────────────────────────
   A cap moved mid-mint sits beside the status, where the eye already is.
   Deliberately loud: it is the one fact on the card that should stop you. */
.cd-flag {
  font-family: var(--mono); font-size: 9px; font-weight: 700;
  letter-spacing: .08em; padding: 1px 5px; border-radius: 3px;
  vertical-align: 1px; white-space: nowrap;
}
.cd-flag.bad  { background: color-mix(in srgb, var(--bad) 18%, transparent);  color: var(--bad); }
.cd-flag.warn { background: color-mix(in srgb, var(--warn) 18%, transparent); color: var(--warn); }
.cd-status { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }

/* Theme sits at the TOP of the panel's list, not in its title bar.
   It was the only control in there without a label — three bare icons wedged
   between "Filters" and the close button, reading as part of the title rather
   than as a setting. As a labelled group it matches View, Show and Money in;
   placing it first keeps it above the fold, which is why it was moved out of
   the list in the first place. */
.ctrl-theme button { font-size: 12.5px; }

/* ── export button, in the phone panel ──────────────────────────────────
   Full width made a once-in-a-while export look like the panel's primary
   action, sitting heavier than any of the filters above it. Sized to its own
   text and left-aligned with the groups, it reads as the secondary thing it
   is. */
@media (max-width: 767px) {
  /* Same width and height as every other bar in the panel, so the column of
     controls reads as one set rather than one odd one out. It was full-width
     but visually heavier than the rest; matching them is what makes it sit
     right, not shrinking it. */
  .controls #exportBtn {
    width: calc(100% - 32px);
    align-self: stretch;
    margin: 26px 16px 18px;
    min-height: 48px;
    font-size: 13px;
    font-family: var(--mono);
    letter-spacing: .04em;
    color: var(--muted);
    background: var(--bg-3);
    border: 1px solid var(--line);
    border-radius: var(--radius);
  }
  .controls #exportBtn:active { background: var(--line); color: var(--text); }

  /* No column headings exist at this width. */
  .sort-hint { display: none; }
}

/* ── stale banner ───────────────────────────────────────────────────────
   A failed refresh used to leave old figures on screen looking live. This
   says plainly that what is below it is no longer current, and when it was
   last true. Amber rather than red: the data is not wrong, it is old. */
.stalebar {
  padding: 9px 20px; font-size: 12.5px; line-height: 1.45;
  background: color-mix(in srgb, var(--warn) 14%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--warn) 35%, transparent);
  color: var(--text);
}
.stalebar b { color: var(--warn); }
.stalebar[hidden] { display: none; }
/* Drain the colour out of live figures while they are known to be old, so a
   stale number cannot be mistaken for a fresh one at a glance. */
body.is-stale .cd-velo,
body.is-stale .dot.live { opacity: .45; }
body.is-stale .dot.live { animation: none; }

/* ── first-load skeleton ────────────────────────────────────────────────
   Six placeholder cards instead of an empty screen. Sized to the real card so
   the list does not jump when the data lands. */
.cd.skel { pointer-events: none; }
.sk-art, .sk-line, .sk-bar {
  background: var(--bg-3); border-radius: 4px;
  animation: skpulse 1.4s ease-in-out infinite;
}
.sk-art { width: 46px; height: 46px; border-radius: calc(var(--radius) - 2px); }
.sk-line { height: 10px; margin-bottom: 8px; }
.sk-line.w60 { width: 60%; } .sk-line.w45 { width: 45%; } .sk-line.w30 { width: 30%; }
.sk-line.w15 { width: 15%; } .sk-line.w10 { width: 10%; }
/* Desktop first-paint skeleton: eight shimmer rows shaped like the table. */
.skel-table { padding: 8px 20px; pointer-events: none; }
.skel-row { display: flex; align-items: center; gap: 14px; padding: 11px 0; border-bottom: 1px solid var(--line); }
.skel-row .sk-line { margin-bottom: 0; }
.sk-art.sm { width: 30px; height: 30px; flex: 0 0 auto; }
.sk-bar { height: 5px; margin: 10px 0; border-radius: 999px; }
@keyframes skpulse { 0%,100% { opacity: 1; } 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) {
  .sk-art, .sk-line, .sk-bar { animation: none; }
}

/* ══════════════════════════════════════════════════════════════════════
   TYPE SCALE — phone
   Nine and ten pixel labels are legible on a desktop monitor at arm's
   length and marginal on a phone held at reading distance, especially the
   uppercase mono ones where every glyph is the same width. Nothing here
   changes layout: only the smallest text moves, by a point or two, and the
   whole scale is in rem so the OS text-size setting still applies.
   ══════════════════════════════════════════════════════════════════════ */
@media (max-width: 767px) {
  .cd-stat i        { font-size: 10px; }     /* was 9  */
  .cd-flag          { font-size: 10px; }     /* was 9  */
  .cd-status        { font-size: 11px; }     /* was 10 */
  .cd-stat b        { font-size: 12.5px; }   /* was 11.5 */
  .sortbar-l        { font-size: 11px; }     /* was 10 */
  .pill             { font-size: 12px; }     /* was 11 */
  .controls .tabs[data-label]::before,
  .controls .ctrl-field[data-label]::before { font-size: 11px; }  /* was 10 */
  .feedsec > summary { font-size: 13px; }
  .cd-name          { font-size: 15.5px; }
  /* Never block pinch-zoom; some text will always be too small for someone. */
  body { -webkit-text-size-adjust: 100%; }
}

/* ---------- expandable floor / sales chart ---------- */

/* The chevron is a control inside a row that is itself clickable, so it needs
   a real target of its own — 28px here, 44px on touch below. */
/* A 12px glyph in its own column was easy to miss entirely. Bigger, bordered,
   and it now sits under a "Graph" heading so the column explains itself. */
.chev {
  background: var(--bg-2); border: 1px solid var(--line); color: var(--muted);
  border-radius: 7px; cursor: pointer; font-size: 15px; line-height: 1;
  width: 34px; height: 30px; display: inline-flex;
  align-items: center; justify-content: center;
  transition: transform .15s ease, color .15s ease, border-color .15s ease;
}
.chev:hover { color: var(--text); border-color: var(--line-2); background: var(--bg-3); }
.chev:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 1px; }
.chev[aria-expanded="true"] { transform: rotate(180deg); color: var(--accent-2); }
@media (prefers-reduced-motion: reduce) { .chev { transition: none; } }

.cd-chev {
  position: absolute; right: 8px; bottom: 6px;
  width: 44px; height: 44px;      /* touch target on the card */
}
.cd-body { position: relative; }

/* The expanded row spans the whole table, so it gets no cell padding of its
   own — the panel inside provides it. */
.ch-row > td { padding: 0 !important; background: var(--bg-2); }
.ch-row:hover { background: var(--bg-2); }

.ch-wrap {
  padding: 12px 14px 14px;
  border-top: 1px solid var(--line);
  background: var(--bg-2);
}
.cd .ch-wrap {
  margin: 12px -13px -13px; padding: 12px 13px 13px;
  border-radius: 0 0 calc(var(--radius) - 1px) calc(var(--radius) - 1px);
  border-top: 1px solid var(--line);
  background: var(--bg);
}

.ch-legend {
  display: flex; gap: 16px; flex-wrap: wrap;
  font-size: 11.5px; color: var(--muted); margin-bottom: 8px;
}
.ch-legend span { display: inline-flex; align-items: center; gap: 6px; }
.ch-key { width: 10px; height: 3px; border-radius: 2px; flex: none; }
.ch-k-sales { background: var(--chart-sales); }
.ch-k-floor { background: var(--chart-floor); }

/* position:relative so the tooltip can be placed against the plot rather than
   the page, and the SVG scales to whatever width the row gives it. */
.ch-plot { position: relative; width: 100%; }
.ch-svg { width: 100%; height: 240px; display: block; overflow: visible; }

.ch-grid { stroke: var(--line); stroke-width: 1; opacity: .5; }
.ch-ax { fill: var(--dim); font-size: 9.5px; font-variant-numeric: tabular-nums; }
.ch-ax-l { text-anchor: end; fill: var(--chart-sales); }
.ch-ax-r { text-anchor: start; fill: var(--chart-floor); }

.ch-line { fill: none; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; }
.ch-sales { stroke: var(--chart-sales); }
.ch-floor { stroke: var(--chart-floor); }
.ch-area { fill: var(--chart-sales); opacity: .12; stroke: none; }

.ch-cross { stroke: var(--dim); stroke-width: 1; stroke-dasharray: 3 3; opacity: .7; }
.ch-dot { stroke: var(--bg); stroke-width: 2; }
.ch-dot-sales { fill: var(--chart-sales); }
.ch-dot-floor { fill: var(--chart-floor); }



.ch-empty, .ch-note { color: var(--muted); font-size: 12px; padding: 6px 2px; }
.ch-note { color: var(--dim); font-size: 11px; padding-top: 8px; }

@media (max-width: 767px) {
}

/* ---------- chart v2: labelled control, readout, draggable scrubber ------- */

/* Card actions row. The chart control was a 12px chevron absolutely positioned
   over the stats, which read as punctuation rather than a button — nobody
   could tell it was tappable. It is now a labelled bar across the card. */
.cd-actions {
  display: flex; gap: 8px; align-items: stretch;
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--bg-3);
}
.cd-graph {
  flex: 1; min-height: 44px;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  background: var(--bg-3); border: 1px solid var(--line);
  border-radius: 8px; color: var(--muted);
  font-size: 13px; font-weight: 550; cursor: pointer;
}
.cd-graph:hover, .cd-graph:focus-visible { color: var(--text); border-color: var(--line-2); }
.cd-graph[aria-expanded="true"] { color: var(--chart-sales); border-color: var(--chart-sales); }
.cd-graph-arrow { transition: transform .15s ease; font-size: 11px; }
.cd-graph[aria-expanded="true"] .cd-graph-arrow { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) { .cd-graph-arrow { transition: none; } }

/*
 * Beside the star in the header, styled as its twin: same 44px tap target and
 * circular hit area, borderless. Deliberately smaller than the star (13px to
 * its 19px) so the pair reads as "keep · lose" rather than two equal buttons —
 * hiding a row is the rarer intent and should not shout.
 *
 * It was previously a full-width bordered button in the actions row under
 * "Graph analysis", which gave a minor browser-local preference the visual
 * weight of a primary action, and put it nowhere near the star it belongs with.
 */
.cd-hide {
  background: none; border: 0; cursor: pointer; color: var(--line-2);
  font-size: 13px; line-height: 1; flex: none;
  min-width: 40px; min-height: 44px; margin: -11px -10px -11px 0;
  display: grid; place-items: center; border-radius: 50%;
}
.cd-hide:hover, .cd-hide:focus-visible { color: var(--bad); }
.cd-hide:focus-visible { outline: 2px solid var(--accent-2); outline-offset: -4px; }

/* Values live above the plot, not in a tooltip over it. A tooltip on a phone
   sits under the finger that summoned it. */
/*
 * The value readout. Sized to its contents, not to the row.
 *
 * As a full-width block it stretched the whole 1273px of a desktop table row —
 * a long empty bar above the chart holding three short numbers.
 */
.ch-readout {
  display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 4px 16px;
  width: max-content; max-width: 100%;
  padding: 0 2px 8px; margin-bottom: 4px;
  background: none; border: 0; border-radius: 0;
  font-size: 13px;
}
.ch-ro-t { color: var(--dim); font-size: 11.5px; flex-basis: 100%; }
.ch-ro-v { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.ch-ro-l { color: var(--muted); }
.ch-ro-v b { font-variant-numeric: tabular-nums; }

/*
 * Scrubber. Rail and handle are HTML, not SVG shapes.
 *
 * The viewBox is scaled to whatever width the container gives it — about 0.4x
 * on a phone — so an SVG circle drawn at r="9" came out SEVEN screen pixels
 * across, well under a fingertip. In HTML the handle is 28px wherever it
 * renders, inside a 44px target.
 *
 * Positioned against .ch-plot and inset by the plot's own margins, so 0%–100%
 * spans exactly the drawn axis rather than the whole SVG box. The insets are
 * CH.l / CH.W and CH.r / CH.W from chart.js.
 */
/*
 * Touch only. A mouse already scrubs by hovering the plot, so on desktop the
 * rail and handle are a control that duplicates something better — and they
 * sat under the chart taking space for no gain.
 */
.ch-rail, .ch-handle { display: none; }

@media (max-width: 767px) {
  /* Inset to the plot's own margins — 56px for the left axis labels, 60px for
     the right — so the rail spans exactly the drawn time axis and the handle
     lines up with the crosshair. Pixels, not percentages: the viewBox is now
     built at the rendered size, so those margins are fixed however wide the
     chart gets. */
  /* bottom: 6px — the SVG's bottom margin now reserves a lane above this for
     the time that travels with the handle (AXIS_TIME_UP in chart.js), so the
     rail sits below the date instead of across it. */
  .ch-rail {
    position: absolute; left: 56px; right: 60px; bottom: 6px; height: 3px;
    background: var(--line-2); border-radius: 999px; pointer-events: none;
    display: block;
  }
  .ch-handle {
    position: absolute; bottom: -14px; left: 0;
    width: 44px; height: 44px; margin-left: -22px;
    display: grid; place-items: center;
    background: none; border: 0; padding: 0;
    cursor: grab; touch-action: none; z-index: 2;
  }
}
.ch-handle::after {
  content: ''; width: 28px; height: 28px; border-radius: 50%;
  background: var(--chart-sales); border: 3px solid var(--bg);
  box-shadow: 0 1px 6px rgba(0, 0, 0, .35);
}
.ch-handle:focus-visible { outline: 2px solid var(--chart-sales); outline-offset: -4px; border-radius: 10px; }
.ch-plot.is-scrubbing { cursor: grabbing; }
.ch-plot.is-scrubbing .ch-handle { cursor: grabbing; }

.ch-surface { fill: transparent; cursor: crosshair; touch-action: none; }
.ch-svg { touch-action: pan-y; }        /* vertical page scroll still works */

.ch-pt { stroke: var(--bg); stroke-width: 1.2; }
.ch-pt-floor { fill: var(--chart-floor); }

@media (max-width: 767px) {
  /* Taller on a phone, not shorter. The chart was 200px with two lines and a
     time axis crammed into it, which is where "the data is so small" came
     from — there was no room for the shape to be visible. */
  .ch-svg { height: 300px; }
  .ch-readout { font-size: 13.5px; }
  .ch-legend { font-size: 11px; }
  .cd .ch-wrap { margin: 12px -12px -12px; padding: 12px; }
}

/* Chart interval picker, in the legend row. */
.ch-int { display: inline-flex; align-items: center; gap: 5px; color: var(--muted); }
.ch-int-sel {
  background: var(--bg-3); border: 1px solid var(--line); border-radius: 6px;
  color: var(--text); font-size: 11.5px; padding: 2px 4px; cursor: pointer;
}
.ch-int-sel:focus-visible { outline: 2px solid var(--chart-sales); outline-offset: 1px; }
/*
 * Scoped to .ch-legend, to outrank it.
 *
 * The hint lives inside the legend, and `.ch-legend span` sets a display —
 * that is a class PLUS an element, which outranks a bare `.ch-hint-drag` no
 * matter which media query it sits in. So "hide on desktop" silently lost
 * every time and the phone instruction "pinch to zoom" was shown to people
 * using a mouse. Matching the legend in the selector makes these two classes
 * against one class-and-element, which wins.
 */
@media (max-width: 767px) { .ch-legend .ch-hint-drag { display: inline; } }
@media (min-width: 768px) { .ch-legend .ch-hint-drag { display: none; } }

/* Value labels pinned to the marked point. paint-order draws the stroke UNDER
   the fill, so the background-coloured outline reads as a halo, not dirt. */
.ch-lbl {
  font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
  paint-order: stroke; stroke: var(--bg); stroke-width: 3px; stroke-linejoin: round;
  pointer-events: none;
}
.ch-lbl-sales { fill: var(--chart-sales); }
.ch-lbl-floor { fill: var(--chart-floor); }

/* ─── the desktop toolbar, as designed geometry rather than accident ───
   Every control was carrying its own padding — segmented tabs 3+5px, inputs
   7px, the export button 7px — so the row came out as boxes of five different
   heights, wrapping wherever the viewport happened to cut. One height, one
   gap, a deliberate row break, and the hint pushed to the far edge. */
@media (min-width: 768px) {
  /* Centred, as a deliberate composition — the rows read as one designed
     block in the middle of the page rather than a pile against the left
     edge. The hint leaves the flow so the centring is true. */
  .controls { row-gap: 8px; column-gap: 8px; padding: 10px 20px 12px; justify-content: center; position: relative; }

  /* The wrappers exist for the phone panel's labels; on the toolbar they must
     not add a box of their own or the inputs fall out of the shared height. */
  .controls .ctrl-field { display: contents; }

  .ctrl-break { flex-basis: 100%; height: 0; margin: 0; }

  .controls .tabs,
  .controls .btn,
  .controls input[type=search],
  .controls select { height: 34px; box-sizing: border-box; }

  .controls .tabs { align-items: center; }
  .controls .tabs button {
    height: 26px; padding: 0 12px;
    display: inline-flex; align-items: center;
    font-size: 13px; white-space: nowrap;
  }

  .controls input[type=search] { width: 280px; min-width: 0; padding: 0 12px; }
  .controls select { padding: 0 28px 0 10px; }
  .controls .btn { padding: 0 14px; font-size: 13px; }

  /* Its own full-width line at the foot of the toolbar, right-aligned. It used
     to be position:absolute bottom-right, which collided with the Export button
     across the whole 1280–1435px band (most laptops) because the centred second
     row reached into the pinned corner. In flow on its own line it can never
     overlap, at any width, and still reads as a quiet note under the controls. */
  .sort-hint { flex-basis: 100%; text-align: right; margin-top: 2px; }
}
@media (max-width: 767px) { .ctrl-break { display: none; } }

/* ─── slide-down alerts ───
   Fixed top-centre, above EVERYTHING (z 80).

   This sat at 45, deliberately under the detail drawer at 50 — the thinking
   being that an open sheet should not be covered. In practice that is exactly
   backwards: a reader with a collection card or the AI chat open is the reader
   most likely to care that something just sold out, and the alert slid in
   silently behind the sheet where it could not be seen at all. On a phone,
   where a sheet fills the screen, it was invisible every time.

   An alert is a transient interruption that dismisses itself after twenty
   seconds and carries its own close button, so it can afford to be on top.
   A column stack with newest first, so several read as a tidy pile. */
/*
 * The stack is an opaque SHELF, not a transparent rail the bars float on.
 *
 * It used to paint nothing itself, so anything not covered by a bar's own box
 * showed the header straight through: the status pills appeared sliced in
 * half below the last bar, and the theme dots peeked over the top through the
 * safe-area strip. Giving the stack the bars' own background — and, via
 * --alert-cover, a floor set to the header's height — makes it read as one
 * solid surface that replaces the header for as long as it is up.
 *
 * pointer-events are AUTO here for the same reason: the covered header is
 * invisible, and letting taps fall through to controls nobody can see is a
 * trap. The bar swallows them until it is dismissed.
 */
.alert-stack {
  position: fixed; top: 0; left: 0; right: 0; z-index: 80;
  display: flex; flex-direction: column;
  /* Desktop keeps the original behaviour: a bare rail that the bars sit on,
     so only the bars themselves are opaque and the header shows either side.
     The full-width shelf below is a PHONE fix — there the header is 117px of
     brand, pills and filters that a 60px bar cannot cover, and the leftovers
     poked out around it. On desktop nothing poked out, so nothing to cover. */
  pointer-events: none;
}
/* No alerts, no shelf — otherwise an empty stack paints a bar across the top. */
.alert-stack:empty { display: none; }
/* A bar, not a floating toast. Full width, flat, square — the same shelf
   language as the header it slides over, told apart by the accent edge and
   a mono tag chip instead of an emoji. */
.alert {
  pointer-events: auto;
  display: flex; align-items: center; gap: 12px;
  padding: 9px 16px;
  background: var(--bg-2);
  border: 0; border-bottom: 1px solid var(--line);
  /* Each bar casts its own shadow on desktop, where there is no shelf behind
     it. On a phone the shelf casts one instead — see the media query. */
  box-shadow: 0 6px 20px rgba(0, 0, 0, .25);
  cursor: pointer;
  opacity: 0; transform: translateY(-100%);
  transition: opacity .18s ease, transform .18s ease;
}
.alert-in { opacity: 1; transform: none; }
.alert-out { opacity: 0; transform: translateY(-100%); pointer-events: none; }
@media (prefers-reduced-motion: reduce) { .alert { transition: none; transform: none; } }

/* No coloured rail. The tag already names the kind and carries the colour;
   a stripe beside it repeats that in a shape every dashboard template uses.
   What distinguishes the two kinds here is the WORD, which is the thing worth
   reading anyway. */

/* A label, not a chip in a box. Small caps and wide tracking read as a
   category heading — the way a masthead marks a section — and it leaves the
   headline the full width instead of wrapping it around a badge. */
.alert-tag {
  flex: none; font-family: var(--mono); font-size: 9.5px; font-weight: 700;
  letter-spacing: .16em; white-space: nowrap; padding: 0;
}
/* The tag is the only thing that carries kind, so it carries the colour too. */
/* Sky blue — its own hue; --chart-sales collided with price-cut's blue. */
.alert-riser .alert-tag { color: #0ea5e9; }
.alert-sellout .alert-tag { color: var(--bad); }
/* A mint still open is the one alert you can act on, so it gets the go
   colour — distinct at a glance from a resale climb or a closed drop. */
.alert-minting .alert-tag { color: var(--good); }
/* Early demand — a market forming while you can still mint: also actionable, but
   its own emerald so it stands apart from a plain open mint. */
.alert-earlydemand .alert-tag { color: #10b981; }
/* Confirmed demand — deeper teal, so it reads as "the emerald one above,
   upgraded" rather than as a separate kind. The amber below is its own hue. */
.alert-confirmed .alert-tag { color: #0d9488; }
/* PEAK DEMAND (kind key stays `topsignal`) — the rarest classification; gold
   because it is scarce, not because it predicts anything. */
.alert-topsignal .alert-tag { color: #d97706; font-weight: 800; }
.alert-paidcrowd .alert-tag { color: #f59e0b; }
/*
 * The two FOREST kinds. Both row tags used to fall through to the plain
 * .alert-tag colour above — HEATING UP and MARKET OPEN came out in the same
 * ink, on the tier where telling them apart matters most. Each now wears the
 * hue of its own filter chip, so the dot in the filter bar and the tag on the
 * row are one colour per kind. Tokens, not literals: see the top of the file.
 */
.alert-heating .alert-tag { color: var(--kind-heat); }
.alert-market_open .alert-tag { color: var(--kind-market); }
/* CHECK-IN follows those two an hour later and must not be mistaken for a
   third alert — its own hue, quieter than either. */
.alert-checkin .alert-tag { color: var(--kind-checkin); }


/* The whole point of an alert is the sentence it carries, so it WRAPS rather
   than truncating — "Xyz is #3 in fastest secon…" told nobody anything. The
   bar grows a line instead; flex-wrap keeps the title and the detail from
   fighting over one row, and overflow-wrap handles the long unspaced names
   collections like to have. */
.alert-tx {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 2px 8px;
  min-width: 0; font-size: 13px;
}
.alert-tx b { color: var(--text); overflow-wrap: anywhere; }
/* pre-line: the away-digest writes one action per line — honour its \n. Notes
   without newlines wrap exactly as before. */
.alert-tx span { color: var(--muted); font-size: 12px; overflow-wrap: anywhere; white-space: pre-line; }

/*
 * The buyer clause, and only the clause.
 *
 * When the clause is the part worth catching the eye, those few words are
 * coloured while the bar, the accent edge and the RISING chip stay exactly as
 * they are for every other alert. Recolouring the whole bar made a normal rank
 * alert read as a different kind of event. Otherwise the clause inherits the
 * muted body colour.
 */
.alert-buyers { color: inherit; }
.alert-buyers.is-narrow { color: var(--alert-narrow); font-weight: 600; }

.alert-go { margin-left: auto; flex: none; color: var(--accent-2); font-size: 12.5px; white-space: nowrap; }
.alert-x {
  flex: none; width: 30px; height: 30px; border: 0; border-radius: var(--radius);
  background: none; color: var(--dim); font-size: 13px; cursor: pointer;
}
.alert-x:hover { background: var(--bg-3); color: var(--text); }
.alert:focus-visible { outline: 2px solid var(--accent-2); outline-offset: -2px; }

@media (max-width: 767px) {
  .alert-tx { flex-direction: column; gap: 1px; }
  .alert-go { display: none; }        /* the whole bar is the tap target */
  /* The hidden "Analyze →" was the element that carried margin-left:auto, so
     without it the ✕ sat wherever the text ended — mid-bar on short alerts.
     The auto margin must live on the ✕ itself here. */
  .alert-x { margin-left: auto; }
  /*
   * A fixed tag COLUMN, not an inline chip. Stacked bars with inline tags
   * started their text wherever the tag happened to end — "RISING"'s headline
   * sat 20% left of "MINTING FAST"'s, and the stack read as jagged. With a
   * fixed column every headline starts at the same x; a two-word tag wraps
   * inside its own column instead of pushing the text over.
   */
  .alert-tag { width: 62px; white-space: normal; line-height: 1.45; }
  /* Swiping a bar sideways must not fight the page's vertical scroll: the
     browser keeps pan-y, the horizontal gesture belongs to the bar. */
  .alert { touch-action: pan-y; }
}

/* The load-more sentinel under a windowed list. */
.more-rows {
  display: block; width: 100%; margin-top: 10px; padding: 11px;
  background: var(--bg-2); border: 1px dashed var(--line-2); border-radius: 9px;
  color: var(--muted); font-size: 13px; cursor: pointer;
}
.more-rows:hover { color: var(--text); border-color: var(--dim); }

/* The drawer's copy of the chart: the section provides the frame, so the wrap
   sheds the row-variant chrome and sits flush inside it. */
.drawer-chart { padding: 0; border-top: 0; background: none; }

/* Alerts on a phone: clear the status-bar / notch area and keep a visible
   breathing space above the first bar instead of welding it to the screen edge. */
@media (max-width: 767px) {
  /*
   * The shelf, phone only. top stays 0 so the background covers the notch
   * strip itself — the inset is padding INSIDE the painted area, not a gap
   * above it — and --alert-cover (set from the header's measured height in
   * alerts.js) carries it down to the header's bottom edge so the status
   * pills cannot show below the last bar.
   *
   * pointer-events go back to auto here because the covered header is
   * invisible: letting taps through to controls nobody can see is a trap.
   */
  .alert-stack {
    padding-top: calc(env(safe-area-inset-top, 0px) + 6px);
    min-height: var(--alert-cover, 0px);
    background: var(--bg-2);
    border-bottom: 1px solid var(--line);
    box-shadow: 0 6px 20px rgba(0, 0, 0, .25);
    pointer-events: auto;
  }
  /* The shelf carries the shadow and the bottom rule here, so the bars must
     not double up: stacked shadows read as a smear, and a last-bar border
     would draw a line partway up the shelf. */
  .alert { padding: 11px 14px; box-shadow: none; }
  .alert:last-child { border-bottom: 0; }
}

/* ─── ask the AI ───
   The trigger is a small mark, not a word: it sits in a 54px column on
   desktop and beside the star on a card, where a label would not fit. */
.askbtn, .cd-ask {
  background: none; border: 0; cursor: pointer; padding: 2px 6px;
  color: var(--accent-2); font-size: 15px; line-height: 1;
  border-radius: var(--radius);
}
.askbtn:hover, .cd-ask:hover { background: var(--bg-3); }
.askbtn:focus-visible, .cd-ask:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 1px; }

/*
 * On a card it gets the same 44px target as the star beside it — at the
 * inherited 25x20 it was both too small to hit and, because the star carries
 * a negative right margin to hug the card edge, shunted onto its own line.
 * The star gives that margin up; the ask button is the last thing in the row
 * now, so the edge is its to own.
 */
/* The ✕ owns the right edge now (star and cross swapped); the negative right
   margin pulls its 40px tap target flush to the card edge. */
.cd-hide { margin-right: -10px; }
.cd-ask {
  font-size: 12px; font-weight: 600; letter-spacing: .2px;
  flex: none; padding: 0 9px; margin: -11px 0;
  min-height: 44px; white-space: nowrap;
  display: inline-flex; align-items: center; gap: 4px;
  border-radius: 999px; color: var(--dim);
}
.cd-ask:hover { color: var(--text); }
.cd-ask:focus-visible { outline-offset: -4px; }

/* The sheet. Same geometry and motion as the info drawer, one layer above it
   so a question asked from an open card lands on top rather than behind. */
#askDrawer {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 70;
  width: min(560px, 100%);
  display: flex; flex-direction: column;
  background: var(--bg); border-left: 1px solid var(--line);
  transform: translateX(100%); visibility: hidden;
  transition: transform .22s cubic-bezier(.3, .8, .4, 1), visibility 0s .22s;
}
#askDrawer.on { transform: none; visibility: visible; transition: transform .22s cubic-bezier(.3, .8, .4, 1), visibility 0s; }
#askScrim { z-index: 65; }
@media (prefers-reduced-motion: reduce) { #askDrawer { transition: none; } }

.ask-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
  padding: 16px 18px; border-bottom: 1px solid var(--line); flex: none;
}
.ask-head-btns { display: flex; align-items: center; gap: 6px; flex: none; }
.ask-clear {
  background: none; border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--muted); font-size: 11.5px; padding: 4px 9px; cursor: pointer; white-space: nowrap;
}
.ask-clear:hover { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 45%, transparent); }
.ask-title { font-family: var(--mono); font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--dim); }
/* A button now, so it must shed the button chrome and keep the heading's look —
   it should read as the title it is, and reveal itself on hover as a way in. */
.ask-name {
  display: block; width: fit-content; max-width: 100%;
  padding: 0; border: 0; background: none; text-align: left;
  font: inherit; font-size: 17px; font-weight: 600; color: var(--text);
  margin-top: 2px; overflow-wrap: anywhere; cursor: pointer;
  border-bottom: 1px solid transparent;
}
.ask-name:hover, .ask-name:focus-visible { color: var(--accent); border-bottom-color: currentColor; }

/* The log grows and scrolls; the composer stays put. */
.ask-log { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 16px 18px; }
.ask-msg { font-size: 14px; line-height: 1.6; margin-bottom: 12px; white-space: pre-wrap; overflow-wrap: anywhere; }
.ask-user {
  background: var(--bg-3); border-radius: 12px 12px 3px 12px;
  padding: 9px 12px; margin-left: auto; max-width: 85%; width: fit-content;
}
/*
 * The answer needs no rail either.
 *
 * A coloured stripe down the left of the reply was the same decoration the
 * notification rows carried, doing the same non-job: the two speakers are
 * already unmistakable, because the question is a right-aligned bubble and the
 * answer is full-bleed prose. Marking that difference twice added a bar of
 * colour and took 12px off the reading width of the thing people came for.
 *
 * What separates them now is the one honest signal — who is talking — set as a
 * small label above the reply. The answers are long and technical, so the
 * width matters more than the ornament did.
 */
/*
 * The answer gets a container too, mirroring the question.
 *
 * Stripped of its rail the reply became bare text against the drawer, which
 * read as unfinished next to the question sitting in a rounded bubble — one
 * speaker framed, the other not. A shared shape says these are two turns of
 * one conversation; the mirrored corner and the opposite alignment say who is
 * speaking, without a stripe of colour doing it.
 *
 * Wider than the question on purpose: answers here run to paragraphs and
 * tables of numbers, and a 85% cap would set them in a column.
 */
.ask-assistant {
  color: var(--text);
  background: var(--bg-2);
  border: 1px solid var(--line);
  /* Mirrors .ask-user's 12px/3px corner, flipped to the other side. */
  border-radius: 12px 12px 12px 3px;
  padding: 11px 13px;
  margin-right: auto;
  max-width: 96%;
  width: fit-content;
}
.ask-assistant + .ask-user { margin-top: 16px; }
/* A quiet attribution inside the box. Only on the answer: the question is
   already the reader's own words, in their own bubble. */
.ask-assistant::before {
  content: 'WATCHER';
  display: block;
  font-family: var(--mono); font-size: 9.5px; font-weight: 700;
  letter-spacing: .16em; color: var(--dim);
  margin-bottom: 6px;
}

/*
 * Formatting inside an answer.
 *
 * white-space: pre-wrap suits raw text and fights real markup — it turns every
 * newline between block elements into an extra blank line — so a rendered
 * answer opts out and lets the blocks do the spacing.
 */
.ask-assistant { white-space: normal; }
.ask-assistant p { margin: 0 0 8px; }
.ask-assistant p:last-child { margin-bottom: 0; }
.ask-assistant strong { font-weight: 650; color: var(--text); }
.ask-assistant code {
  font-family: var(--mono); font-size: 12px;
  background: var(--bg-3); padding: 1px 5px; border-radius: 4px;
}
.ask-list { margin: 0 0 8px; padding-left: 18px; }
.ask-list li { margin-bottom: 3px; }
.ask-list:last-child { margin-bottom: 0; }

/* A table of numbers is the reason the renderer exists. It scrolls inside its
   own box so a wide one never makes the drawer scroll sideways. */
.ask-tablewrap { overflow-x: auto; margin: 0 0 8px; -webkit-overflow-scrolling: touch; }
.ask-table { border-collapse: collapse; font-size: 12.5px; min-width: 100%; }
.ask-table th, .ask-table td {
  padding: 5px 10px; text-align: left; white-space: nowrap;
  border-bottom: 1px solid var(--line);
}
.ask-table th {
  font-family: var(--mono); font-size: 10px; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; color: var(--dim);
}
/* Figures line up when they are tabular; a column of prices that jitters is
   harder to compare than one that does not. */
.ask-table td { font-variant-numeric: tabular-nums; }
.ask-table tr:last-child td { border-bottom: 0; }

.ask-hint { font-size: 13px; line-height: 1.6; color: var(--muted); }
.ask-eg { display: flex; flex-direction: column; gap: 6px; margin-top: 14px; align-items: flex-start; }
.ask-chip {
  background: var(--bg-2); border: 1px solid var(--line); border-radius: 999px;
  color: var(--text); font-size: 12.5px; padding: 6px 12px; cursor: pointer; text-align: left;
}
.ask-chip:hover { border-color: var(--accent-2); color: var(--accent-2); }

.ask-form {
  display: flex; gap: 8px; padding: 12px 18px;
  border-top: 1px solid var(--line); flex: none;
  padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
}
/* A textarea, so Shift+Enter has a new line to insert. It starts one row tall
   and is grown to fit by script, up to the max-height, then scrolls. */
.ask-form textarea {
  flex: 1; min-width: 0; background: var(--bg-2); color: var(--text);
  border: 1px solid var(--line-2); border-radius: 9px; padding: 10px 12px;
  font: inherit; font-size: 14px; line-height: 1.45; outline: none;
  resize: none; overflow-y: auto; max-height: 132px;
}
.ask-form textarea:focus { border-color: var(--accent-2); }
.ask-form textarea:disabled { opacity: .6; }
/* Bottom-aligned, so the Ask button stays level with the last line as the
   box grows upward rather than floating in the middle of it. */
.ask-form { align-items: flex-end; }
/* The Ask button matches the one-row textarea's exact height (10px padding ×2
   + 14px × 1.45 line + 2px borders ≈ 42px) and centers its label, so the pair
   reads as one control on the phone instead of a short button floating beside
   a taller box. It stays bottom-pinned (align-items above) as the box grows. */
.ask-form .btn {
  flex: none; height: 42px; padding-top: 0; padding-bottom: 0;
  display: inline-flex; align-items: center;
}

/* Full-height sheet on a phone, matching the info card's treatment. */
@media (max-width: 767px) {
  #askDrawer {
    width: 100%; top: auto; height: 92dvh; max-height: 92dvh;
    border-left: 0; border-top: 1px solid var(--line); border-radius: 16px 16px 0 0;
    transform: translateY(100%);
  }
  #askDrawer.on { transform: none; }
  .ask-log { padding: 14px; }
  .ask-form { padding-left: 14px; padding-right: 14px; }
}

/*
 * Project Sauce.
 *
 * The summary used to unfold inside the fact grid — a two-column layout of
 * one-line values — so a paragraph was set in a column about forty characters
 * wide and ran for a screen and a half on a phone. It reads in its own surface
 * now, and .sauce-body is only where the text is kept between renders.
 */
.sauce-btn { font-size: 12px; padding: 4px 10px; }
.sauce-body { display: none; }
.sauce-src { font-size: 11px; color: var(--dim); line-height: 1.55; }
.sauce-src a { color: var(--accent-2); }

.sauce-scrim {
  position: fixed; inset: 0; background: color-mix(in srgb, #000 38%, transparent);
  opacity: 0; pointer-events: none; transition: opacity .18s; z-index: 76;
}
.sauce-scrim.on { opacity: 1; pointer-events: auto; }

/*
 * Desktop: a card over the page, above the info drawer and the ask sheet so
 * it is readable from either, and below the alert stack so a mint landing
 * mid-read still shows.
 */
.sauce-sheet {
  position: fixed; z-index: 78;
  left: 50%; top: 50%;
  width: min(600px, calc(100vw - 48px));
  max-height: min(660px, calc(100vh - 80px));
  display: flex; flex-direction: column;
  background: var(--bg); border: 1px solid var(--line);
  border-radius: 16px;
  box-shadow: 0 24px 64px rgba(16, 24, 40, .28);
  overflow: hidden;
  transform: translate(-50%, calc(-50% + 10px)); opacity: 0;
  transition: transform .19s cubic-bezier(.3, .8, .4, 1), opacity .19s;
}
.sauce-sheet.on { transform: translate(-50%, -50%); opacity: 1; }
.sauce-sheet[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) { .sauce-sheet { transition: none; } }

.sauce-sheet-head {
  flex: none; display: flex; align-items: center; gap: 12px;
  padding: 16px 18px 14px; border-bottom: 1px solid var(--line);
  background: var(--bg-2);
}
.sauce-sheet-titles { flex: 1; min-width: 0; }
.sauce-eyebrow {
  font-size: 10px; letter-spacing: .13em; text-transform: uppercase;
  color: var(--dim); font-weight: 600;
}
.sauce-sheet-name {
  margin-top: 3px; font-size: 17px; font-weight: 650; letter-spacing: -.2px;
  color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sauce-back { display: none; }
.sauce-close {
  flex: none; width: 30px; height: 30px; border-radius: 8px;
  background: none; border: 1px solid var(--line); color: var(--muted);
  font-size: 13px; line-height: 1; cursor: pointer;
}
.sauce-close:hover { color: var(--text); border-color: var(--muted); }

.sauce-sheet-body {
  flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  padding: 20px 22px 22px;
}
/* A reading measure, not a data cell: larger, looser, and capped in width. */
.sauce-sheet-text {
  margin: 0 0 13px; max-width: 62ch;
  font-size: 15px; line-height: 1.68; color: var(--text);
}
.sauce-sheet-text:last-of-type { margin-bottom: 0; }
.sauce-sheet-foot {
  margin-top: 20px; padding-top: 14px; border-top: 1px solid var(--line);
  display: flex; flex-direction: column; gap: 7px;
}

/*
 * Phone: the next page rather than a card. The collection's info sheet stays
 * mounted underneath exactly as it was, this covers it, and back or a swipe
 * down returns to it with the reader's scroll position intact.
 */
@media (max-width: 720px) {
  .sauce-scrim { display: none; }
  .sauce-sheet {
    left: 0; top: 0; right: 0; bottom: 0;
    width: auto; max-height: none; border: 0; border-radius: 0;
    box-shadow: none; background: var(--bg);
    transform: translateX(100%); opacity: 1;
    transition: transform .22s cubic-bezier(.3, .8, .4, 1);
  }
  .sauce-sheet.on { transform: none; }
  .sauce-sheet-head {
    padding: 14px 14px 12px;
    padding-top: max(14px, env(safe-area-inset-top));
  }
  .sauce-back {
    display: block; flex: none; width: 32px; height: 32px; border-radius: 8px;
    background: none; border: 1px solid var(--line); color: var(--text);
    font-size: 20px; line-height: 1; padding: 0; cursor: pointer;
  }
  .sauce-close { display: none; }
  .sauce-sheet-body {
    padding: 18px 16px calc(28px + env(safe-area-inset-bottom));
  }
  .sauce-sheet-text { font-size: 15.5px; line-height: 1.7; max-width: none; }
}

/* Chart v3: volume bars and the in-view summary. */
.ch-bar { fill: var(--chart-sales); opacity: .78; }
.ch-sum { font-size: 12.5px; color: var(--muted); margin-bottom: 6px; }
.ch-sum b { color: var(--text); font-variant-numeric: tabular-nums; }

/* Pinch-zoom reset, in the legend row. Hidden until a pinch zooms in —
   [hidden] must win over the display we set, or it never disappears. */
.ch-reset {
  background: var(--bg-3); border: 1px solid var(--line); border-radius: 6px;
  color: var(--text); font-size: 11px; padding: 2px 8px; cursor: pointer;
}
.ch-reset:hover { border-color: var(--chart-sales); }
.ch-reset[hidden] { display: none; }

/* The moment under the crosshair, riding along the plot's bottom edge.
   Muted on purpose: it is context for the two value labels, not a third
   value. The .ch-lbl halo keeps it readable over bars. */
.ch-time { fill: var(--muted); font-size: 10.5px; font-weight: 600; }

/* ─── notification bell + history panel ───
   The alert bars auto-dismiss in twenty seconds; the bell is where they
   land afterwards. Same shelf language as the bars themselves: flat,
   square, accent edge per kind, mono tag chips — a list of the same bars,
   parked. */
/* Built to the same recipe as the theme switcher beside it — .tabs' bg-3
   well, 1px line, 8px radius, 3px inset — so the two read as one pair of
   controls rather than a pill next to a box. The icon sits in an inner
   6px-radius plate, which is what .tabs button uses for its selected state. */
.bell {
  display: inline-flex; align-items: center; justify-content: center;
  position: relative; flex: none;
  background: var(--bg-3); border: 1px solid var(--line); border-radius: 8px;
  padding: 3px; cursor: pointer; color: var(--muted);
}
.bell svg {
  display: block; padding: 4px 9px; border-radius: 6px;
  box-sizing: content-box; width: 18px; height: 18px;
}
.bell[hidden] { display: none; }
.bell:hover { color: var(--text); }
.bell:hover svg { background: var(--bg); }
.bell[aria-expanded="true"] { color: var(--text); }
.bell[aria-expanded="true"] svg {
  background: var(--bg); box-shadow: 0 1px 2px rgba(16, 24, 40, .08);
}
.bell:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 2px; }
.bell-n {
  position: absolute; top: -5px; right: -5px;
  background: var(--accent); color: var(--bg);
  font-family: var(--mono); font-size: 10px; font-weight: 700; font-style: normal;
  min-width: 17px; height: 17px; border-radius: 999px;
  display: grid; place-items: center; padding: 0 4px;
}
.bell-n[hidden] { display: none; }

/* On a phone the theme switcher is not in the bar — the bell's neighbour is
   the 44px hamburger, so it matches THAT instead and keeps a full touch
   target. */
@media (max-width: 767px) {
  .bell {
    width: 40px; height: 40px; padding: 0;
    background: none; border-radius: var(--radius); color: var(--text);
  }
  .bell svg { padding: 0; width: 20px; height: 20px; }
  .bell:hover svg, .bell[aria-expanded="true"] svg { background: none; box-shadow: none; }
}

/* Above the header (30) and the alert bars (45), below nothing it overlaps.
   The recent-chats panel is the same object in a different context, so it
   shares every rule here rather than restating them. */
.bell-panel, .chats-panel {
  position: fixed; z-index: 62;
  top: 64px; right: 16px;
  width: min(420px, calc(100vw - 32px));
  max-height: min(70vh, 560px);
  overflow-y: auto; overscroll-behavior: contain;
  background: var(--bg-2); border: 1px solid var(--line);
  box-shadow: 0 14px 36px rgba(0, 0, 0, .3);
  opacity: 0; transform: translateY(-6px);
  transition: opacity .16s ease, transform .16s ease;
}
/* The collection name inside a recent-chat row: a second destination, so it
   has to look like one without turning the row into a wall of links. */
.chat-name {
  display: block; width: fit-content; max-width: 100%;
  padding: 0; border: 0; background: none;
  font: inherit; font-weight: 700; color: var(--fg);
  text-align: left; cursor: pointer;
  border-bottom: 1px solid transparent;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chat-name:hover, .chat-name:focus-visible { color: var(--accent); border-bottom-color: currentColor; }

/*
 * A slim overlay scrollbar, the way the phone already draws one.
 *
 * Left alone, a desktop browser gives this panel the platform scrollbar: a
 * full-width grey trough with a chunky thumb, welded to the right edge inside
 * the panel's own border. It reads as a second container bolted onto the
 * notifications rather than part of them. The phone has always drawn a thin
 * dark thumb over the content with no track at all, and that is the one to
 * match — same treatment .tscroll already uses for the wide tables.
 */
/*
 * A slim thumb with nothing behind it, the way the phone already draws one.
 *
 * Left alone, a desktop browser gives this panel the platform scrollbar: a
 * 17px grey trough with a chunky thumb, welded inside the panel's own border.
 * It reads as a second container bolted onto the notifications rather than
 * part of them.
 *
 * The two ways to change that are mutually exclusive, and that is the whole
 * subtlety here. Chrome ignores every ::-webkit-scrollbar rule on an element
 * whose scrollbar-color is anything but auto — so declaring both left the
 * standard property winning and drawing its own bar, wider than the default
 * rather than narrower. Each engine therefore gets exactly one mechanism:
 * Firefox the standard properties, which are all it has; everything with the
 * pseudo-elements gets those, at 6px with a transparent track.
 */
.bell-panel, .chats-panel {
  /* Derived from --text, so it is dark on a light ground and light on a dark
     one — as an overlay scrollbar behaves — instead of a fixed grey that
     vanishes in one theme and shouts in the other. */
  --tray-thumb: color-mix(in srgb, var(--text) 30%, transparent);
}
@supports not selector(::-webkit-scrollbar-thumb) {
  .bell-panel, .chats-panel {
    scrollbar-width: thin;
    scrollbar-color: var(--tray-thumb) transparent;
  }
}
.bell-panel::-webkit-scrollbar, .chats-panel::-webkit-scrollbar {
  width: 6px; background: transparent;
}
.bell-panel::-webkit-scrollbar-track, .chats-panel::-webkit-scrollbar-track {
  background: transparent; border: 0; box-shadow: none;
}
.bell-panel::-webkit-scrollbar-thumb, .chats-panel::-webkit-scrollbar-thumb {
  background: var(--tray-thumb); border-radius: 999px;
}
.bell-panel::-webkit-scrollbar-thumb:hover, .chats-panel::-webkit-scrollbar-thumb:hover {
  background: color-mix(in srgb, var(--text) 48%, transparent);
}
/* No arrow buttons, and no dead square where two bars would meet. */
.bell-panel::-webkit-scrollbar-button, .chats-panel::-webkit-scrollbar-button,
.bell-panel::-webkit-scrollbar-corner, .chats-panel::-webkit-scrollbar-corner {
  display: none;
}

.bell-panel.on, .chats-panel.on { opacity: 1; transform: none; }
.bell-panel[hidden], .chats-panel[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) {
  .bell-panel, .chats-panel { transition: none; transform: none; }
}

.bell-head {
  position: sticky; top: 0; z-index: 1;
  display: flex; align-items: center; gap: 8px;
  padding: 10px 12px 10px 14px;
  background: var(--bg-2); border-bottom: 1px solid var(--line);
}
.bell-head > span {
  flex: 1; font-family: var(--mono); font-size: 11px; font-weight: 700;
  letter-spacing: .12em; text-transform: uppercase; color: var(--dim);
}
.bell-sound {
  background: none; border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--muted); font-size: 11.5px; padding: 4px 9px; cursor: pointer;
  white-space: nowrap;
}
.bell-sound:hover { color: var(--text); border-color: var(--dim); }
.bell-sound[aria-pressed="true"] { color: var(--accent-2); border-color: color-mix(in srgb, var(--accent-2) 45%, transparent); }

.bell-clear {
  background: none; border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--muted); font-size: 11.5px; padding: 4px 9px; cursor: pointer;
  white-space: nowrap;
}
.bell-clear:hover { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 45%, transparent); }
.bell-close {
  display: grid; place-items: center; width: 30px; height: 30px;
  background: none; border: 0; border-radius: var(--radius);
  color: var(--dim); font-size: 13px; cursor: pointer;
}
.bell-close:hover { background: var(--bg-3); color: var(--text); }

/*
 * Two lines, not a badge bolted to the side of a paragraph.
 *
 * The rail-plus-chip layout was the shape every dashboard template ships, and
 * it worked against the content: a boxed tag sat in the text column, so a
 * headline like "Cash Rabbits is #3 in fastest secondary sales" wrapped around
 * it onto three lines while a coloured stripe repeated, in decoration, what
 * the tag already said in words.
 *
 * These are events, so they are set like entries in a log — a quiet label and
 * the time on one line, the sentence given the full width beneath it. The kind
 * is carried by the label alone. Grid rather than flex because the DOM order
 * (tag, text, time, dismiss) is the reading order for a screen reader and
 * should not have to change to get the visual arrangement right.
 */
.bell-row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  column-gap: 10px; row-gap: 3px;
  padding: 11px 14px 13px;
  border-bottom: 1px solid var(--line);
  cursor: pointer;
  transition: background .12s;
}
.bell-row:last-child { border-bottom: 0; }
.bell-row:hover { background: var(--bg-3); }
.bell-row:focus-visible { outline: 2px solid var(--accent-2); outline-offset: -2px; }
.bell-row .alert-tag { grid-area: 1 / 1; }
/* The label cell of an alert row: the kind tag plus, when the server sent one,
   the "?" that folds out the explanation. Wallet and ask rows have no header
   wrapper, so their tag keeps the grid-area above. */
.bell-row .bell-hd   { grid-area: 1 / 1; display: flex; align-items: center; gap: 6px; min-width: 0; }
.bell-row .bell-ago  { grid-area: 1 / 2; }
.bell-row .bell-x    { grid-area: 1 / 3; }
.bell-row .bell-tx   { grid-area: 2 / 1 / 3 / 4; }

.bell-tx { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
/* Wraps, like the bars — the history is read, not glanced at, so a clipped
   headline is worse here than an extra line. */
.bell-tx b { font-size: 13px; color: var(--text); overflow-wrap: anywhere; }
.bell-tx span { font-size: 12px; color: var(--muted); overflow-wrap: anywhere; white-space: pre-line; }

/* Sits on the label line now, so it needs no nudge to align with anything —
   and tabular figures stop the column jittering as "9m" becomes "10m". */
.bell-ago {
  flex: none; font-family: var(--mono); font-size: 10px; color: var(--dim);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* Per-entry dismiss. Full-size touch target on a phone; on desktop it fades
   up only when the row is under the pointer, so a quiet list stays quiet. */
.bell-x {
  flex: none; width: 24px; height: 24px; margin: 0 -6px 0 0;
  border: 0; border-radius: var(--radius); background: none;
  color: var(--dim); font-size: 12px; cursor: pointer;
  opacity: 0; transition: opacity .12s;
}
.bell-row:hover .bell-x, .bell-x:focus-visible { opacity: 1; }
.bell-x:hover { background: var(--bg-2); color: var(--text); }
@media (hover: none) { .bell-x { opacity: 1; width: 34px; height: 34px; } }

.bell-empty { padding: 22px 16px; color: var(--muted); font-size: 13px; line-height: 1.5; }

/*
 * No change window on a phone.
 *
 * The deltas need a second line under every value, and the phone layout is
 * already cards rather than columns — there is nowhere for them to go that
 * does not push the actual figures apart. The control is hidden rather than
 * merely unstyled so it cannot be reached at all on a narrow screen.
 */
@media (max-width: 767px) {
  .controls .sel:has(#change) { display: none; }
}

/* On the phone the panel is a full-width sheet under the header — a narrow
   floating box on a 375px screen is neither one thing nor the other. */
@media (max-width: 767px) {
  .bell-panel, .chats-panel {
    top: calc(env(safe-area-inset-top, 0px) + 60px);
    left: 10px; right: 10px; width: auto;
    max-height: calc(100dvh - 120px);
  }
}

/* Why there is no summary yet — printed, not left blank. "Their site is a
   link tree" is an answer; a silent gap is indistinguishable from nobody
   having looked. */
.sauce-why { grid-column: 1 / -1; margin-top: 6px; font-size: 11.5px; color: var(--dim); line-height: 1.5; }

/* A floor coming off the live listing book, so a cached snapshot can never be
   mistaken for a current price. Sized to sit beside digits without disturbing
   the odometer's baseline. */
.live-dot {
  display: inline-block;
  width: 5px; height: 5px;
  margin-left: 5px;
  border-radius: 50%;
  background: var(--good);
  vertical-align: middle;
}
@media (prefers-reduced-motion: no-preference) {
  .live-dot { animation: livePulse 2.4s ease-in-out infinite; }
}
@keyframes livePulse { 0%, 100% { opacity: 1 } 50% { opacity: .35 } }

/* ─────────────────────── tracked wallets + callouts ─────────────────────── */


/* The card itself — one design both on phone and desktop. */
.tw-modal {
  position: fixed; inset: 0; z-index: 220;
  display: flex; align-items: center; justify-content: center;
  background: rgba(10, 14, 12, .45); backdrop-filter: blur(2px);
}
.tw-card {
  width: min(420px, calc(100vw - 28px));
  background: var(--bg); border: 1px solid var(--line-2); border-radius: 14px;
  padding: 14px 16px 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .35);
}
.tw-head { display: flex; align-items: center; justify-content: space-between; font-weight: 700; }
.tw-close { border: 0; background: none; color: var(--dim); font-size: 15px; cursor: pointer; padding: 4px; }
.tw-hint { color: var(--muted); font-size: 12.5px; line-height: 1.45; margin: 8px 0 10px; }
.tw-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; }
.tw-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: 7px 10px; border: 1px solid var(--line); border-radius: 9px; background: var(--bg-2);
}
.tw-row code { font-size: 12.5px; }
.tw-del { border: 0; background: none; color: var(--dim); cursor: pointer; }
.tw-del:hover { color: var(--bad); }
.tw-none { color: var(--dim); font-size: 12.5px; padding: 4px 2px; }

/* The away-summary popup: the digest's full receipt, wallet by wallet.
   Shares the tw-modal scrim and card; only the body inside differs. */
.tw-away .tw-card { width: min(460px, calc(100vw - 28px)); }
.tw-away .tw-count { color: var(--muted); font-weight: 400; font-size: 12.5px; }
.tw-away-body {
  display: flex; flex-direction: column; gap: 12px;
  margin-top: 12px; max-height: min(62vh, 520px); overflow-y: auto;
}
.tw-away-w b { display: block; margin-bottom: 4px; font-size: 14px; }
.tw-away-li { color: var(--muted); font-size: 13px; line-height: 1.55; padding-left: 2px; }
.tw-away-net {
  border-top: 1px solid var(--line); padding-top: 10px;
  font-weight: 700; font-size: 13.5px;
}
/* A collection name in the summary is a door — dress it like one. */
.tw-away-a { color: var(--accent-2); cursor: pointer; }
.tw-away-a:hover { text-decoration: underline; }

/* ── one wallet's notification log ── */
.tw-logcard { width: min(480px, calc(100vw - 28px)); padding-bottom: 8px; }
/* The chip bar borrows .co-sortbar's design; inside a card it drops the
   full-bleed strip background and wraps instead of overflowing. */
.tw-lfbar { flex-wrap: wrap; padding: 10px 0 8px; background: none; margin-top: 2px; }
.tw-lfbar .co-sort i { font-style: normal; opacity: .6; font-weight: 400; }
/*
 * The log is the tray's own rows, unchanged — same layout, same tag, same
 * spacing. An earlier pass gave them outlined pills and a coloured rail down
 * each row; against a list already grouped by one wallet and filtered to one
 * action, that decoration described nothing the reader had not just chosen,
 * and read as ornament. The card supplies the frame; the rows supply the
 * content.
 */
.tw-logbody { max-height: min(62vh, 540px); overflow-y: auto; margin: 0 -4px; }
/* (The burner rail that this rule used to cancel inside the log is gone from
   the whole board now — see .tw-burner-row further down. Nothing to cancel.) */

/* Minted rows never had a kind colour — teal-blue, distinct from buy green. */
.tw-row-mint .alert-tag { color: var(--accent-2); }
/* On its own line under the name, so it never wraps around the title. */
.tw-burner { align-self: flex-start; }

/* The log glyph on a wallet's group head — sits before the count. */
.tw-log {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 24px; height: 24px; border: 1px solid var(--line);
  border-radius: 7px; color: var(--dim); font-size: 13px; cursor: pointer;
  background: var(--bg-2);
}
.tw-log:hover { color: var(--text); border-color: var(--line-2); }

/* The Alerts tab's type chips ride above the rows, same strip as Callouts'.
   Each chip wears its kind's own colour — the colour the notification rows
   already speak — so "Sold out is red" holds before AND after you tap.
   Padding matches the rows' 14px so the first chip's edge sits exactly on
   the line the alert tags below start from. Six kinds, six HUES — theme
   variables collide (riser and price-cut both resolved to the same blue,
   warn doubles up in orchard), so the four non-semaphore kinds carry their
   own mid-tone hexes, readable on both light and dark grounds. */
/*
 * Padding must beat .co-sortbar's, which is declared LATER in this file and
 * therefore won at equal specificity — that 12px, against the head's 14px,
 * is why "All" sat two pixels left of "Alerts" however often it was nudged.
 * Two classes on the same element makes this rule win outright, and the
 * values are the head's own so the chip row and the tab row share one edge:
 * both boxes start at 14px, both labels at 14 + 9px of inner padding.
 */
/*
 * An even GRID at every width, not just on a phone.
 *
 * The grid was written inside the mobile media query, so desktop kept the old
 * flex wrap: chips sized to their own text, five on one line, three on the
 * next, every gap a different width. auto-fit with a minimum column keeps the
 * columns equal and lets the count follow the panel — four across a wide
 * tray, three on a narrow one — without a second set of rules to maintain.
 */
/*
 * The column MINIMUM is set by the widest chip's contents, not by a round
 * number. At 92px the 420px tray fitted four columns of 74px of usable width
 * while the longest chip ("Early demand 🔒") needs ~101px — every locked chip
 * overflowed its own column and got sliced by the overflow:hidden below.
 * 104px lands three columns in the tray (≈107px usable), which clears the
 * longest label, its dot and its lock with room to spare.
 */
.co-sortbar.bell-filterbar {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
  gap: 6px; align-items: stretch;
  padding: 8px 12px 8px 14px;
}
/*
 * REDESIGNED (2026-08-19): the old bar coloured each label's TEXT in its
 * kind's hue — eleven rainbow words at eleven visual weights, which read as
 * noise, not navigation ("poorly designed… doesn't match our standards").
 * Now every chip is the same quiet pill in the app's text colour, and the
 * kind's hue lives in a small leading DOT — colour as a key, not as type.
 * The selected chip fills with a whisper of its hue and brightens its border,
 * so the active filter is obvious without shouting. "All" carries no dot:
 * it is the absence of a filter, not a kind.
 */
/*
 * ONE UNBREAKABLE UNIT: dot, label and lock are flex items of the chip itself,
 * so they cannot be split across a wrap and cannot drift out of the pill. The
 * chip may not shrink below its own dot + lock, and everything that has to
 * give is given by the LABEL (min-width:0 + ellipsis on .cs-label), never by
 * the glyphs. text-overflow used to sit here on the button, where it could
 * never fire — a flex container is not a block container, so the anonymous
 * text item was hard-clipped instead of elided, and the clip fell mid-lock.
 */
.bell-filterbar .co-sort {
  --hue: var(--dim);
  display: flex; align-items: center; justify-content: center; gap: 5px;
  min-width: 0;
  color: var(--text); opacity: .72; padding: 6px 7px;
  overflow: hidden; white-space: nowrap;
  transition: opacity .12s ease, background-color .12s ease, border-color .12s ease;
}
.bell-filterbar .co-sort::before {
  content: ''; flex: none; width: 7px; height: 7px; border-radius: 50%;
  background: var(--hue);
}
/* The only elastic part of a chip. */
.bell-filterbar .cs-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* The lock never shrinks and never wraps away from its label. Sized a shade
   under the label so a 1.37em-wide emoji does not dominate an 11.5px word. */
.bell-filterbar .cs-lock { flex: none; font-size: 10px; line-height: 1; }
.bell-filterbar .co-sort:hover { opacity: 1; }
/*
 * LOCKED = GREYED IN PLACE, AND STILL READABLE.
 *
 * A locked chip is an advert for the next tier, so it has to be legible; it
 * only has to look unavailable. .tier-locked's own opacity:.4 never applied
 * here (this selector outranks it), so "locked" was carried by grayscale(1)
 * alone — a no-op on a label already painted in var(--text). Say it once,
 * explicitly, at a dim that still clears 4.5:1 on both grounds: the label
 * keeps the text colour, the KIND DOT is what goes grey, because the dot is
 * the thing the tier buys.
 */
.bell-filterbar .co-sort.tier-locked {
  opacity: .66; filter: none;
  color: var(--text);
}
.bell-filterbar .co-sort.tier-locked::before { background: var(--dim); }
.bell-filterbar .co-sort.tier-locked .cs-lock { opacity: .85; }
.bell-filterbar .co-sort.tier-locked:hover { opacity: .88; }
.bell-filterbar .co-sort.on {
  opacity: 1;
  background: color-mix(in srgb, var(--hue) 13%, var(--bg));
  border-color: color-mix(in srgb, var(--hue) 55%, var(--line-2));
}
.bell-filterbar .co-sort[data-afilter="all"]::before { display: none; }
.bell-filterbar .co-sort[data-afilter="all"] { --hue: var(--accent); }
.bell-filterbar .co-sort[data-afilter="topsignal"] { --hue: #d97706; }
.bell-filterbar .co-sort[data-afilter="earlydemand"] { --hue: #4ade80; }
.bell-filterbar .co-sort[data-afilter="confirmed"] { --hue: #0d9488; }
.bell-filterbar .co-sort[data-afilter="heating"] { --hue: var(--kind-heat); }
/* The two Forest chips must not be the same dot. market_open had no rule at
   all, so it inherited --hue's var(--dim) default and read as permanently
   locked; see --kind-market at the top of this file. */
.bell-filterbar .co-sort[data-afilter="market_open"] { --hue: var(--kind-market); }
.bell-filterbar .co-sort[data-afilter="checkin"] { --hue: var(--kind-checkin); }
.bell-filterbar .co-sort[data-afilter="floor_lifter"] { --hue: #a78bfa; }
.bell-filterbar .co-sort[data-afilter="minting"] { --hue: var(--good); }
.bell-filterbar .co-sort[data-afilter="paidcrowd"] { --hue: #f59e0b; }
.bell-filterbar .co-sort[data-afilter="price_raise"] { --hue: #f43f5e; }
.bell-filterbar .co-sort[data-afilter="riser"] { --hue: #0ea5e9; }
.bell-filterbar .co-sort[data-afilter="sellout"] { --hue: var(--bad); }

/* The rows speak the exact same six hues, tray and toast both. */
.tw-addrow { display: flex; gap: 8px; }
.tw-input {
  flex: 1; min-width: 0; padding: 8px 10px; border-radius: 9px;
  border: 1px solid var(--line-2); background: var(--bg-2); color: var(--text); font: inherit; font-size: 13px;
}
.tw-add {
  padding: 8px 14px; border-radius: 9px; border: 1px solid var(--accent);
  background: var(--accent); color: #fff; font: inherit; cursor: pointer;
}
.tw-add:disabled { opacity: .45; cursor: default; }
.tw-err { color: var(--bad); font-size: 12px; margin: 8px 0 0; }

/* A tracked wallet's notification: leaf green, with a sparkle sweeping across
   on repeat — unmistakably "one of yours" before a word is read. The sweep
   lives on ::after so it never touches the text. */
.alert.alert-wallet, .bell-row.alert-wallet {
  background: color-mix(in srgb, var(--good) 14%, var(--bg));
  border-color: color-mix(in srgb, var(--good) 45%, var(--line-2));
  position: relative; overflow: hidden;
}
.alert-wallet .alert-tag { color: var(--good); }
.alert.alert-wallet::after {
  content: ''; position: absolute; top: 0; bottom: 0; width: 46px;
  background: linear-gradient(105deg, transparent, color-mix(in srgb, var(--good) 38%, transparent) 50%, transparent);
  left: -60px; pointer-events: none;
  animation: twSweep 2.6s ease-in-out infinite;
}
@keyframes twSweep { 0% { left: -60px; } 60%, 100% { left: calc(100% + 20px); } }

/* The wallet name copies its address. Dotted underline says "I do a thing",
   without shouting like a link; the confirmation swaps in place for a beat. */
.tw-ghead .tw-copy { cursor: copy; text-decoration: underline dotted; text-underline-offset: 3px; }
.tw-ghead .tw-copy:hover { color: var(--text); }
/* The burner chip copies its address — say so with the cursor, not a tooltip
   nobody hovers on a phone. */
.tw-burner.tw-copy { cursor: copy; }
.tw-burner.tw-copy:hover { color: var(--text); border-color: var(--line-2); }
.tw-copied { color: var(--good) !important; }
.tw-ghead .tw-copied { color: var(--good); text-decoration: none; }

/* Per-token profit inside a sell row, and per-wallet realised P/L on the
   group head. Green up, red down — the one place semantic colour belongs. */
.tw-pct { font-style: normal; font-weight: 650; }
.tw-pct.up { color: var(--good); }
.tw-pct.down { color: var(--bad); }
.tw-pnl {
  /* The P/L chip is the single divider between the left cluster (name, burners)
     and the right cluster (log, count). It carries the ONE auto margin and gets
     a fixed min-width with right-aligned digits, so its value ends at the same
     x on every wallet regardless of what sits to its left — Jojo's −$65 and
     Bobo's +$1,961 line up instead of floating to wherever their chips ended. */
  margin-left: auto; margin-right: 8px; flex: none;
  min-width: 92px; text-align: right;
  font-family: var(--mono); font-size: 10.5px; font-weight: 650;
  white-space: nowrap; color: var(--muted); font-variant-numeric: tabular-nums;
}
.tw-pnl.up { color: var(--good); }
.tw-pnl.down { color: var(--bad); }
/* The group head normally pushes its count right; with a P/L chip present the
   chip carries the auto margin instead and the count follows it. */
.tw-ghead .tw-pnl + .tw-gcount { margin-left: 0; }

/* The gear's P/L-window chips. */
.bs-pnl { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.bs-pnl .bs-hint { flex-basis: 100%; }
.bs-win {
  border: 1px solid var(--line-2); background: none; color: var(--muted);
  font-family: var(--mono); font-size: 11px; padding: 3px 9px;
  border-radius: 999px; cursor: pointer;
}
.bs-win:hover { color: var(--text); border-color: var(--dim); }
.bs-win.on { color: var(--good); border-color: color-mix(in srgb, var(--good) 55%, var(--line-2)); font-weight: 700; }
@media (prefers-reduced-motion: reduce) { .alert.alert-wallet::after { animation: none; display: none; } }

/* Clear-all chip over a stacked pile of toasts. Phones only: that is where a
   backlog physically covers the board. Everything cleared stays in the tray. */
.alert-clearall {
  display: none; align-self: flex-end;
  padding: 6px 12px; border-radius: 999px; border: 1px solid var(--line-2);
  background: var(--bg); color: var(--text); font: inherit; font-size: 12px; cursor: pointer;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .25);
}
@media (max-width: 900px) { .alert-stack .alert-clearall { display: block; } }

/* Tabs in the tray head: Alerts | Callouts, with the unseen count highlighted
   until the tab is opened. */
.bell-tabs { display: flex; gap: 4px; margin-right: auto; }
.bell-tab {
  border: 1px solid transparent; background: none; color: var(--dim);
  font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer;
  padding: 4px 9px; border-radius: 8px; position: relative;
}
.bell-tab.on { color: var(--text); background: var(--bg-3); border-color: var(--line); }
.co-n {
  display: inline-block; min-width: 16px; padding: 1px 4px; margin-left: 5px;
  border-radius: 999px; background: var(--good); color: #fff;
  font-size: 10.5px; font-style: normal; text-align: center; font-weight: 700;
}

/* A callout row: who is being called, then the two live tallies. The voter's
   own side stays lit so "tap again to switch" explains itself. */
.co-row {
  display: flex; align-items: center; gap: 8px;
  padding: 9px 12px; border-bottom: 1px solid var(--line); cursor: pointer;
}
.co-tx { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.co-tx b { font-size: 13px; }
.co-tx span { color: var(--dim); font-size: 11.5px; }
.co-vote {
  border: 1px solid var(--line-2); background: var(--bg-2); color: var(--text);
  font: inherit; font-size: 12px; cursor: pointer; padding: 5px 9px; border-radius: 8px;
}
.co-vote.co-up.my, .co-vote.co-up:hover { border-color: var(--good); color: var(--good); }
.co-vote.co-down.my, .co-vote.co-down:hover { border-color: var(--bad); color: var(--bad); }

/* The card's callout button and its two-choice popover. */
.cd-callout {
  border: 1px solid var(--line-2); background: var(--bg-2); color: var(--text);
  font: inherit; font-size: 12.5px; cursor: pointer; padding: 7px 11px; border-radius: 9px;
}
.cd-callout:hover { border-color: var(--accent); color: var(--accent); }
.co-pick {
  display: inline-flex; gap: 6px; margin-left: 8px;
}
.co-pick button {
  border: 1px solid var(--line-2); background: var(--bg); font: inherit; font-size: 12.5px;
  cursor: pointer; padding: 7px 11px; border-radius: 9px;
}
.co-pick button[data-dir="up"] { color: var(--good); border-color: color-mix(in srgb, var(--good) 55%, var(--line-2)); }
.co-pick button[data-dir="down"] { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 55%, var(--line-2)); }

/* Language picker: quiet, beside the brand on desktop (after the chain
   sub-mark), re-homed into the filters drawer on phones. */
.brand #langSel { margin-left: 10px; }
#langSel {
  border: 1px solid var(--line-2); background: var(--bg-2); color: var(--text);
  font: inherit; font-size: 12px; border-radius: 8px; padding: 4px 6px; cursor: pointer;
}

/* ── wallet tray, header alignment, language picker redesign ── */

/* THE close bug: .tw-modal declared display:flex, which outranks the UA's
   [hidden]{display:none}, so hiding the modal did nothing. Any element we
   toggle with the hidden attribute must lose its display when hidden. */
.tw-modal[hidden], .tw-panel[hidden], .bell-panel[hidden] { display: none !important; }

/* One control height across the top bar. The wallet button and the language
   picker sat shorter than the bells and the theme tabs — five heights in one
   row reads as clutter, one height reads as a design. */
#langSel { height: 30px; box-sizing: border-box; }

/* The language picker, properly designed: native chrome off, our own chevron,
   the same quiet surface as every other top-bar control, and a focus ring
   instead of the browser's blue halo. */
#langSel {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  padding: 0 22px 0 10px;
  background-color: var(--bg-2);
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%23888'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 8px center;
  font-weight: 600; letter-spacing: .3px;
  /* Center the language code in the visible box (the asymmetric padding above
     exists for the chevron; without this the label hugs the left edge). */
  text-align: center; text-align-last: center;
}
#langSel:hover { border-color: var(--accent); color: var(--accent); }
#langSel:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Wallet bell inherits .bell; the badge goes leaf-green so the two bells read
   as siblings with different jobs, not duplicates. */
.tw-bell .tw-n { background: var(--good); }

/* The wallet tray reuses the bell-panel design wholesale — same head, same
   rows — with a managing strip on top: the tracked wallets as chips, and the
   add row inline. One surface for "who am I following" and "what did they do". */
.tw-panel .tw-manage {
  display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
  padding: 10px 12px; border-bottom: 1px solid var(--line);
}
.tw-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 8px; border: 1px solid var(--line-2); border-radius: 999px;
  background: var(--bg-2); font-size: 12px;
}
.tw-chip .tw-del { border: 0; background: none; color: var(--dim); cursor: pointer; padding: 0 2px; }
.tw-chip .tw-del:hover { color: var(--bad); }
.tw-addrow { display: inline-flex; gap: 6px; flex: 1; min-width: 220px; }
.tw-panel .tw-input { flex: 1; min-width: 0; padding: 6px 9px; font-size: 12.5px; }
.tw-panel .tw-add { padding: 6px 12px; }
.tw-panel .tw-err { width: 100%; margin: 4px 0 0; }
.tw-none { color: var(--dim); font-size: 12.5px; }

/* ── notification settings, callout column, transfer effect ── */

/* The gear sits with the head controls; the settings strip drops below. */
.bell-gear {
  border: 0; background: none; color: var(--dim); cursor: pointer;
  font-size: 14px; padding: 4px 6px; border-radius: 7px;
}
.bell-gear:hover, .bell-gear[aria-expanded="true"] { color: var(--text); background: var(--bg-3); }
.bell-settings {
  padding: 8px 12px 10px; border-bottom: 1px solid var(--line);
  display: flex; flex-direction: column; gap: 6px;
}
.bs-hint { color: var(--dim); font-size: 11.5px; line-height: 1.4; margin-bottom: 2px; }
.bs-row {
  display: flex; align-items: center; justify-content: space-between;
  font-size: 12.5px; cursor: pointer; padding: 3px 0;
}
.bs-row input { accent-color: var(--accent); width: 15px; height: 15px; cursor: pointer; }

/*
 * The master switch. Separated by a rule and given a surface of its own,
 * because it is not a seventh category — it is the control that overrides the
 * six above it, and a row identical to theirs would read as one more of them.
 * The label carries a second line saying what silence does NOT cost you.
 */
.bs-all {
  margin-top: 4px; padding-top: 10px; border-top: 1px solid var(--line);
}
.bs-row-all {
  align-items: flex-start; gap: 14px; padding: 11px 16px;
  background: var(--bg-3); border: 1px solid var(--line); border-radius: 10px;
}
.bs-row-all span { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.bs-row-all b { font-size: 12.5px; font-weight: 700; color: var(--text); }
.bs-row-all i { font-style: normal; font-size: 11px; line-height: 1.45; color: var(--dim); }
/* A switch, not a tickbox: this one turns a whole system off. Selector has
   to out-rank `.bs-row input`, which sizes the ordinary checkboxes. */
.bs-row input.bs-allbtn {
  appearance: none; -webkit-appearance: none; flex: none;
  width: 40px; height: 23px; border-radius: 999px; margin-top: 1px;
  background: var(--line-2); border: 0; cursor: pointer; position: relative;
  transition: background .15s;
}
.bs-row input.bs-allbtn::after {
  content: ''; position: absolute; top: 3px; left: 3px;
  width: 17px; height: 17px; border-radius: 50%; background: var(--bg);
  box-shadow: 0 1px 2px rgba(16, 24, 40, .3); transition: transform .15s;
}
.bs-row input.bs-allbtn:checked { background: var(--bad); }
.bs-row input.bs-allbtn:checked::after { transform: translateX(17px); }
.bs-row input.bs-allbtn:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 2px; }
/* When everything is off, the block says so at a glance. */
.bs-all.is-off .bs-row-all {
  border-color: color-mix(in srgb, var(--bad) 45%, var(--line));
  background: color-mix(in srgb, var(--bad) 7%, var(--bg-3));
}
@media (max-width: 767px) {
  /* Thumb-sized on a phone, and the two lines get room to breathe. Two classes,
     because the `.bs-row` reset below is declared later and would otherwise flatten
     the horizontal padding back to 0 — which stuck the label and switch to the edges. */
  .bs-row.bs-row-all { padding: 13px 16px; gap: 12px; }
  .bs-row input.bs-allbtn { width: 46px; height: 27px; }
  .bs-row input.bs-allbtn::after { width: 21px; height: 21px; }
  .bs-row input.bs-allbtn:checked::after { transform: translateX(19px); }
  .bs-row { padding: 6px 0; font-size: 13px; }
  .bs-row input { width: 18px; height: 18px; }
}

/* Callout in the columns: compact glyph button, same family as ✦ and ▾. */
.cd-callout.co-col {
  border: 0; background: none; padding: 4px 8px; font-size: 14px; cursor: pointer;
  border-radius: 7px;
}
.cd-callout.co-col:hover { background: var(--bg-3); }

/*
 * The filter chips on a phone — and ONLY the chips.
 *
 * An earlier version of this block also rebuilt the tray's head: it wrapped
 * the head and pushed the tab strip onto a full second row so the tabs and
 * chips could share one three-column grid. That did align them, and it did
 * fix a real overlap at 360px — but it moved the sound, clear-all and gear
 * controls ABOVE the Alerts / Wallets / Callouts tabs, which reads as the
 * chrome outranking the navigation. The head is reverted to what it was; the
 * tabs keep their own scrolling strip exactly as before.
 *
 * The chips still get the grid, because that is the part that was actually
 * asked for: equal columns mean equal gaps and a tidy block instead of a
 * ragged wrap, without touching anything above them.
 */
@media (max-width: 767px) {
  /*
   * A small screen spends a little of everything — a hair off the type, the
   * dot, the gaps and the lock — to fit more chips across without any of them
   * clipping. Type only; the column COUNT is left to the base rule, because
   * this query also catches a 767px tablet where the tray runs the full width
   * and a phone-sized column minimum would pack six 97px chips into it.
   */
  .bell-filterbar .co-sort { padding: 5px 4px; gap: 4px; font-size: 11px; }
  .bell-filterbar .co-sort::before { width: 6px; height: 6px; }
  .bell-filterbar .cs-lock { font-size: 9px; }
}
/*
 * Phone-sized columns, and only on a phone-sized screen. 96px is what buys
 * THREE chips across a 375px tray with the compact type above. It is a
 * minimum and not a pinned `repeat(3, 1fr)` on purpose: pinned at three, a
 * 320px screen gives 84px of chip and every locked label loses 5–23px to the
 * ellipsis ("Early d…"), which throws away the very thing a locked chip is
 * there to advertise. As a minimum, the bar steps down to two roomy columns
 * by itself once the tray's inner width falls under ~300px — a 366px viewport,
 * allowing for the panel's scrollbar. Raise this number for wider chips
 * sooner; lower it to 90px to hold three across down to ~356px, at the cost
 * of eliding the two longest labels.
 */
@media (max-width: 430px) {
  .co-sortbar.bell-filterbar { grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); }
}

/* Phone card: graph and callout share one line, half and half. The hide
   button drops to its own full-width line below them. */
.cd-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.cd-actions .cd-graph { flex: 1 1 46%; min-width: 0; }
.cd-actions .cd-callout:not(.co-col) { flex: 1 1 46%; min-width: 0; text-align: center; }
/* Ask AI joined this row; full width on its own line so the two toggles above
   pair cleanly and this reads as the third action rather than a stray. */
.cd-actions .cd-ask { flex: 1 1 100%; justify-content: center; margin: 0; min-height: 40px; border: 1px solid var(--line); border-radius: 10px; }
/* Graph and Callout now share the row evenly — hide has moved to the header. */

/* The vote flying to the bell: a fixed-position glyph that translates to the
   bell and fades. Pure transform/opacity, so it costs nothing to animate. */
.co-fly {
  position: fixed; z-index: 300; pointer-events: none;
  font-size: 15px; color: var(--good);
  transition: transform .52s cubic-bezier(.34, .8, .3, 1), opacity .5s ease;
  opacity: 1;
}

/* The callout popover lives on <body>, fixed at the button — inside a table
   cell it inherited the row's click path and clipping. */
.co-pick {
  position: fixed; z-index: 240;
  display: inline-flex; gap: 6px; margin: 0; padding: 6px;
  background: var(--bg); border: 1px solid var(--line-2); border-radius: 10px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, .3);
}

/* Paste next to Add: one tap on a phone instead of a long-press ritual. */
.tw-paste {
  border: 1px solid var(--line-2); background: var(--bg-2);
  font: inherit; font-size: 13px; cursor: pointer; padding: 6px 9px; border-radius: 9px;
}
.tw-paste:hover { border-color: var(--accent); }

/* ── callout sort bar, tab badges, wallet tab bar, bell landing ── */

.co-sortbar {
  display: flex; gap: 4px; padding: 8px 12px;
  border-bottom: 1px solid var(--line); background: var(--bg-3);
}
.co-sort {
  border: 1px solid transparent; background: none; color: var(--dim);
  font: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer;
  padding: 3px 9px; border-radius: 7px;
}
.co-sort.on { color: var(--text); background: var(--bg); border-color: var(--line-2); }
.co-sort:hover { color: var(--text); }

/* The price a call was made at — the number that makes it scoreable later. */
.co-at { color: var(--text); font-variant-numeric: tabular-nums; }
/* the call's verdict at a glance: green if the price went up since, red if down */
.co-move { font-variant-numeric: tabular-nums; }
.co-move.up { color: var(--good, #16a34a); }
.co-move.down { color: var(--bad, #dc2626); }

/* Per-tab counts, tinted to the tab's own meaning. */
.co-n-callouts { background: var(--accent); }
.co-n-wallets { background: var(--good); }
.co-n-alerts { background: var(--warn); color: #1a1200; }

/* The Wallets tab's header strip: how many are tracked, and the way in to
   changing that. Management itself is a card, not a cramped strip. */
.tw-bar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 9px 12px; border-bottom: 1px solid var(--line);
  background: var(--bg-3); font-size: 12.5px; color: var(--muted);
}
.tw-manage-btn {
  border: 1px solid var(--line-2); background: var(--bg); color: var(--text);
  font: inherit; font-size: 12px; cursor: pointer; padding: 4px 11px; border-radius: 8px;
}
.tw-manage-btn:hover { border-color: var(--accent); color: var(--accent); }

/* Card header gains a quiet count, so "5 of 5" explains a disabled add row. */
.tw-count { margin-left: auto; margin-right: 10px; color: var(--dim); font-size: 12px; font-weight: 400; }
.tw-addrow[hidden] { display: none; }
.tw-paste {
  border: 1px solid var(--line-2); background: var(--bg-2); color: var(--text);
  font: inherit; font-size: 12.5px; cursor: pointer; padding: 8px 12px; border-radius: 9px;
}
.tw-paste:hover { border-color: var(--accent); color: var(--accent); }

/* The bell acknowledging a landed vote. */
.bell-hit { animation: bellHit .6s ease; }
@keyframes bellHit {
  0%, 100% { transform: none; }
  30% { transform: scale(1.18) rotate(-8deg); }
  60% { transform: scale(1.06) rotate(5deg); }
}
@media (prefers-reduced-motion: reduce) { .bell-hit { animation: none; } }

/* ── tray chrome fixes: badge, header, scrollbar ── */

/*
 * The badge sat BELOW its tab: .bell-tab was a plain inline button, so the
 * <i> wrapped once the label plus count exceeded the tab's natural width.
 * inline-flex + nowrap keeps label and count on one line as a single unit.
 */
.bell-tab {
  display: inline-flex; align-items: center; gap: 5px;
  white-space: nowrap; line-height: 1;
}
/* A proper pill: fixed height, centred digits, tabular so 1 and 9 are the
   same width, and never taller than the label it sits beside. */
.co-n {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 17px; height: 17px; padding: 0 5px; margin: 0;
  border-radius: 999px; font-size: 10px; font-weight: 700; font-style: normal;
  font-variant-numeric: tabular-nums; letter-spacing: 0;
  background: var(--good); color: #fff; box-shadow: 0 0 0 1px var(--bg-2);
}

/*
 * The ✕ was half under the scrollbar: the panel scrolls, so the overlay
 * scrollbar is drawn over its right edge — including over the sticky header.
 * A gutter reserves that strip so no control is ever beneath it.
 */
.bell-panel, .chats-panel { scrollbar-gutter: stable; }
.bell-head { padding-right: 10px; }
.bell-close { flex: 0 0 auto; margin-left: 2px; }

/* ── the wallet management card ── */
.tw-row { align-items: center; }
.tw-rowid { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.tw-rowid b { font-size: 13px; }
.tw-rowid code { font-size: 11px; color: var(--dim); }
.tw-addrow { flex-direction: column; align-items: stretch; gap: 8px; }
.tw-fields { display: flex; flex-direction: column; gap: 8px; }
.tw-nick {
  padding: 8px 10px; border-radius: 9px; border: 1px solid var(--line-2);
  background: var(--bg-2); color: var(--text); font: inherit; font-size: 13px;
}
.tw-actions { display: flex; gap: 8px; }
.tw-actions .tw-add { flex: 1; }

/* ── grouped feed: wallet, then action ── */
.tw-group { border-bottom: 1px solid var(--line); }
.tw-ghead, .tw-khead {
  display: flex; align-items: center; gap: 8px; width: 100%;
  border: 0; background: none; color: var(--text); font: inherit; cursor: pointer;
  text-align: left; padding: 9px 12px;
}
.tw-ghead { background: var(--bg-3); font-size: 13px; }
.tw-ghead:hover, .tw-khead:hover { background: var(--bg-2); }
.tw-ghead code { color: var(--dim); font-size: 11px; }
/* Name over address, one column. The name may ellipsize; the address is its
   caption. min-width:0 is what lets this cell shrink instead of the row
   overflowing when a nickname is long and the pills stack up after it. */
.tw-id { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; min-width: 0; }
.tw-id b { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tw-id code { font-size: 10px; line-height: 1.2; }
/* Keep the tracked-wallet NAME readable. The min-width:0 above let the id column
   collapse to nothing when a busy wallet's pills (NEW, burners, PNL, count)
   stacked up beside it — the "Bobo cut off to 4px" bug. Reserve the name's own
   width so a short nickname always shows; a long one still truncates via the
   ellipsis on .tw-id b. */
.tw-ghead .tw-id { min-width: auto; }
@media (max-width: 640px) {
  /* On a phone the head is tight. The address line under the name is redundant
     with the nickname (still one tap-to-copy on the name itself), so drop it and
     tighten the row gap — leaving the count and PNL room to sit beside a FULL
     name instead of crushing it. */
  .tw-ghead .tw-idmeta { display: none; }
  .tw-ghead { gap: 5px; }
}
/* Belt and braces on phones: whatever the numbers do, the chip cannot push
   the row wider than the tray. The tooltip always carries the exact figure. */
@media (max-width: 767px) {
  .tw-pnl { max-width: 44vw; overflow: hidden; text-overflow: ellipsis; }
}
.tw-khead { padding: 6px 12px 6px 24px; font-size: 11.5px; }
.tw-kname {
  font-family: var(--mono); font-size: 10px; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase;
}
.tw-caret { color: var(--dim); font-size: 10px; width: 10px; }
.tw-gcount {
  margin-left: auto; color: var(--dim); font-size: 11px;
  font-variant-numeric: tabular-nums;
}
.tw-group .bell-row { padding-left: 24px; }
.tw-tok { color: var(--dim); font-weight: 400; }

/*
 * A colour per action, carried on the tag and a left rail so the kind reads
 * before the words do: money in is green, money out is amber, an offer to
 * sell is teal, an offer to buy is violet.
 */
.tw-row-buy  .alert-tag { color: var(--good); }
.tw-row-sell .alert-tag { color: var(--warn); }
.tw-row-list .alert-tag { color: var(--accent); }
.tw-row-bid  .alert-tag { color: #a78bfa; }
.tw-sub-buy  .tw-kname { color: var(--good); }
.tw-sub-sell .tw-kname { color: var(--warn); }
.tw-sub-list .tw-kname { color: var(--accent); }
.tw-sub-bid  .tw-kname { color: #a78bfa; }

/* ── inline nickname editing ── */
.tw-nickedit {
  font-size: 13px; outline: none; border-radius: 5px;
  padding: 1px 4px; margin: -1px -4px; cursor: text;
  border-bottom: 1px dashed transparent;
}
.tw-nickedit:hover { border-bottom-color: var(--line-2); }
.tw-nickedit:focus { background: var(--bg-3); border-bottom-color: var(--accent); }
.tw-nickedit.tw-unnamed { color: var(--dim); font-weight: 400; font-style: italic; }
.tw-nickedit.tw-unnamed:focus { font-style: normal; color: var(--text); }

/* ── one token, pinned above the collection card ──────────────────────────
   Deliberately the SAME surface, radius and link styling as the card header
   below it, so the two read as one panel rather than a strip stuck on top. */
.tok-strip {
  display: flex; align-items: flex-start; gap: 14px;
  padding: 14px; margin: 0 0 12px;
  background: var(--bg-2); border: 1px solid var(--line);
  /* Square, to sit flush with the collection card beneath it — the rounded
     corners made the strip read as a floating pill rather than the top of
     the same panel. The artwork keeps its own soft radius. */
  border-radius: 0;
}
.tok-imgwrap { display: block; flex: 0 0 auto; line-height: 0; border-radius: 11px; }
.tok-imgwrap:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.tok-img {
  width: 84px; height: 84px; border-radius: 11px;
  object-fit: cover; background: var(--bg-3); border: 1px solid var(--line-2);
  transition: transform .18s ease;
}
.tok-imgwrap:hover .tok-img { transform: scale(1.03); }
.tok-noimg { display: grid; place-items: center; font-size: 26px; }
.tok-tx { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
.tok-coll {
  font-family: var(--mono); font-size: 10px; font-weight: 700;
  letter-spacing: .11em; text-transform: uppercase; color: var(--dim);
}
/* The artwork's own title, printed verbatim — glitch names are real names.
   break-word so a long unbroken one cannot push the card sideways. */
.tok-name { font-size: 17px; line-height: 1.25; overflow-wrap: anywhere; }
.tok-traits { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px; }
.tok-traits i {
  font-style: normal; font-size: 10.5px; color: var(--muted);
  background: var(--bg-3); border: 1px solid var(--line);
  padding: 2px 8px; border-radius: 999px;
}
.tok-traits i b { color: var(--text); font-weight: 600; }
.tok-links { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 8px; }
.tok-links a { font-size: 12.5px; color: var(--accent); text-decoration: none; }
.tok-links a:hover { text-decoration: underline; }
.tok-x { border: 0; background: none; color: var(--dim); font-size: 15px; cursor: pointer; padding: 2px 4px; align-self: flex-start; }
.tok-x:hover { color: var(--text); }
/* The instant skeleton: the placeholder tile breathes until the real art lands,
   so the strip reads as "loading this piece" rather than "no image". */
.tok-noimg.tok-loading { animation: tokPulse 1.1s ease-in-out infinite; }
@keyframes tokPulse { 0%, 100% { opacity: .45; } 50% { opacity: .85; } }
@media (prefers-reduced-motion: reduce) { .tok-noimg.tok-loading { animation: none; } }
.co-nofloor { color: var(--dim); font-style: italic; }

/* Burner activity — the throwaway wallets one hop upstream of a tracked wallet.
   A muted pill so it reads as provenance on the row, never as a second alert. */
.tw-burners {
  font-family: var(--mono); font-size: 10.5px; font-weight: 700;
  color: var(--muted); background: var(--bg-3); border: 1px solid var(--line);
  padding: 1px 7px; border-radius: 999px; margin-left: 6px; white-space: nowrap;
}
/* "Last active Xh ago" on the wallet header. Dim so it reads as metadata, and
   nowrap + a left margin so it never collides with the pinned log button that
   follows it. It joins the left cluster; the log button keeps its auto margin,
   so the count column stays aligned. */
/* Address + "last active" on the caption line beneath the name. A baseline row
   that wraps rather than widening the header, so the pinned log button and the
   count column keep their position no matter how busy the wallet is. */
.tw-idmeta { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; min-width: 0; }
.tw-fresh {
  font-size: 10.5px; color: var(--dim);
  white-space: nowrap; font-variant-numeric: tabular-nums;
}
.tw-burner {
  font-family: var(--mono); font-size: 9.5px; font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase; color: var(--muted);
  background: var(--bg-3); border: 1px solid var(--line);
  /* No left margin: the chip sits on its own line under the collection name
     inside the .bell-tx column, so it must share the name's left edge. The 7px
     it used to carry was from when it rode inline beside the title, and it left
     the tag visibly indented from the name it belongs to. */
  padding: 0 6px; border-radius: 999px; margin-left: 0; vertical-align: 1px;
}
/* No accent rail on burner rows anywhere. The BURNER chip under the collection
   name says it in words; a coloured stripe down the row's left edge said the
   same thing again, in a code the reader had to learn first. */
@media (max-width: 560px) {
  .tok-strip { gap: 11px; padding: 12px; }
  .tok-img { width: 64px; height: 64px; }
  .tok-name { font-size: 15px; }
}

/* A cast vote reads as cast: filled, not outlined. Without this the button
   looked identical before and after voting, which is what made repeated
   presses feel like they were each counting. */
.co-vote.my.co-up {
  background: color-mix(in srgb, var(--good) 18%, transparent);
  border-color: var(--good); color: var(--good); font-weight: 700;
}
.co-vote.my.co-down {
  background: color-mix(in srgb, var(--bad) 18%, transparent);
  border-color: var(--bad); color: var(--bad); font-weight: 700;
}
.co-pick button.my {
  background: color-mix(in srgb, var(--accent) 15%, transparent);
  border-color: var(--accent); font-weight: 600;
}

/*
 * The head controls must look identical on every tab.
 *
 * Alerts carries an extra "Clear all" button, so that row is wider than the
 * other two — and with the default flex-shrink the surplus was taken out of
 * whatever was already there: sound, gear and ✕ came out narrower and shorter
 * on Alerts than on Callouts and Wallets. Same controls, three sizes.
 *
 * The controls are now fixed-size, and the tab strip is the only thing
 * allowed to give ground. It has room to lose — the labels are short and the
 * counts sit inside them.
 */
.bell-head .bell-sound,
.bell-head .bell-clear,
.bell-head .bell-gear,
.bell-head .bell-close { flex: 0 0 auto; }

/* One height for every control in the row, so they sit on a shared baseline
   whatever the tab. */
.bell-head .bell-sound,
.bell-head .bell-clear { height: 26px; display: inline-flex; align-items: center; }
.bell-head .bell-gear {
  width: 26px; height: 26px; display: inline-flex;
  align-items: center; justify-content: center; padding: 0; font-size: 13px;
}
.bell-head .bell-close { width: 26px; height: 26px; }

/* The strip absorbs the squeeze, and scrolls rather than wrapping if a
   translated label runs long. */
.bell-tabs {
  flex: 0 1 auto; min-width: 0; overflow-x: auto;
  scrollbar-width: none; -ms-overflow-style: none;
}
.bell-tabs::-webkit-scrollbar { display: none; }

/*
 * The sound toggle as an icon button — same 26px square as the gear and the
 * ✕ beside it, so the three read as one set of controls rather than a label
 * with two glyphs stuck on the end. Muted is the resting state visually
 * (dim); sound-on lights up, because on is the deliberate choice here.
 */
.bell-head .bell-sound {
  width: 26px; height: 26px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: none; color: var(--dim); border-radius: var(--radius);
}
.bell-head .bell-sound:hover { background: var(--bg-3); color: var(--text); border: 0; }
.bell-head .bell-sound[aria-pressed="true"] { color: var(--accent-2); border: 0; }
.bell-head .bell-sound[aria-pressed="true"]:hover { background: var(--bg-3); }

/*
 * "+2" on the wallet and again on the exact action, so a glance answers
 * "what is new" down to Whale #1 · BOUGHT without opening a fold. Distinct
 * from the plain total beside it: the total is grey and quiet, this is the
 * accent and carries a sign.
 */
.tw-new {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 17px; padding: 0 6px; margin-left: 6px;
  border-radius: 999px; background: var(--good); color: #fff;
  font-size: 10px; font-weight: 700; font-variant-numeric: tabular-nums;
  letter-spacing: 0; line-height: 1;
}
.tw-sub .tw-new { background: color-mix(in srgb, var(--good) 82%, var(--bg)); }

/* The rows themselves: a soft wash so the new ones are findable inside a
   long list, fading out so it never competes with the row's own content. */
.tw-new-row { background: color-mix(in srgb, var(--good) 7%, transparent); }
@media (prefers-reduced-motion: no-preference) {
  .tw-new-row { animation: twNewFade 6s ease-out forwards; }
  @keyframes twNewFade {
    0%, 45% { background: color-mix(in srgb, var(--good) 10%, transparent); }
    100% { background: color-mix(in srgb, var(--good) 4%, transparent); }
  }
}
/* A folded group still says it is holding something new. */
.tw-group.has-new > .tw-ghead b { color: var(--text); }

/*
 * TREND MATCH wears the wallet family's green-and-sparkle — the two share a
 * meaning (something of YOURS: your tracked wallet, your pattern hit) — but
 * keeps its own class so the gear can mute one without the other.
 */
.alert.alert-trend, .bell-row.alert-trend {
  background: color-mix(in srgb, var(--good) 14%, var(--bg));
  border-color: color-mix(in srgb, var(--good) 45%, var(--line-2));
  position: relative; overflow: hidden;
}
/* Teal — was var(--good), indistinguishable from MINTING FAST's green. */
.alert-trend .alert-tag { color: #14b8a6; }
.alert.alert-trend::after {
  content: ''; position: absolute; top: 0; bottom: 0; width: 46px;
  background: linear-gradient(105deg, transparent, color-mix(in srgb, var(--good) 38%, transparent) 50%, transparent);
  left: -60px; pointer-events: none;
  animation: twSweep 2.6s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) { .alert.alert-trend::after { animation: none; display: none; } }

/*
 * These hues belong to the alert ROWS' tags. The three matching .bell-filterbar
 * rules that used to sit alongside them are gone: they were rainbow-era chip
 * rules that survived the 2026-08-19 redesign and, being declared here — far
 * below it — still overrode the chip's `color: var(--text)`. The damage was
 * worst on a locked chip, where .tier-locked's grayscale(1) flattened #7c3aed
 * to a mid grey: "Buying wave" measured 1.68:1 against the tray on the dark
 * theme, against 7.42:1 for every other locked chip — the owner's "faded to
 * near-invisible". The chips take their colour from the leading dot now.
 */
/* PAID MINT — amber-gold: neither the red of a warning nor the green of a go
   signal, so it sits on its own between the two. */
.alert-worth .alert-tag { color: #ca8a04; }
/* FLOOR LIFTERS — deep violet, heaviest weight in the family. */
.alert-floor_lifter .alert-tag { color: #7c3aed; }
/* PRICE RAISED — free→paid bait-and-switch, read live from chain. A warning. */
.alert-price_raise .alert-tag { color: var(--bad); }

/* The Alerts tab's filter head — one line when closed, the grid when open. */
.bell-filterhead {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 14px; cursor: pointer;
  border-bottom: 1px solid var(--line); background: var(--bg-3);
  font-size: 12px; user-select: none;
}
.bell-filterhead:hover { background: var(--bg-2); }
.bell-filterhead .bf-label {
  font-family: var(--mono); font-size: 10px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--dim);
}
.bell-filterhead .bf-current { color: var(--text); font-weight: 600; }
.bell-filterhead .bf-n {
  font-style: normal; font-family: var(--mono); font-size: 10px;
  color: var(--dim); margin-left: 3px;
}
.bell-filterhead .bf-caret { margin-left: auto; color: var(--dim); font-size: 11px; }
.bell-filterhead:focus-visible { outline: 2px solid var(--accent-2); outline-offset: -2px; }
/* Open: the grid sits directly under its head, so they read as one control. */
.bell-filterhead.is-open { border-bottom: 0; }

/* A collection with no artwork yet gets its initials on a colour derived from
   its own address, rather than an empty grey hole that reads as a failure.
   See thumb() in app.js for why the picture is genuinely absent. */
.thumb-ph {
  display: inline-flex; align-items: center; justify-content: center;
  background: hsl(var(--ph-h, 210) 42% 88%);
  border-color: hsl(var(--ph-h, 210) 34% 74%);
  color: hsl(var(--ph-h, 210) 46% 30%);
  font-weight: 700; font-size: 12px; letter-spacing: .02em;
  overflow: hidden; user-select: none;
}
.thumb-ph span { line-height: 1; }
.dhead .thumb-ph { font-size: 26px; }
.feedrow .thumb-ph { font-size: 10px; }
:root:not([data-theme="light"]) .thumb-ph,
html[data-theme="dark"] .thumb-ph, html[data-theme="orchard"] .thumb-ph {
  background: hsl(var(--ph-h, 210) 30% 24%);
  border-color: hsl(var(--ph-h, 210) 26% 34%);
  color: hsl(var(--ph-h, 210) 45% 78%);
}

/*
 * The wallet log button sits in a FIXED place on every row.
 *
 * It was flowing right after the name, so a wallet with extra chips (burner
 * count, P/L) pushed it far right while a bare wallet left it hard against
 * its own nickname — the same control landing in two different places one
 * row apart. Pinned beside the total instead, so the eye finds it once.
 */
.tw-ghead .tw-log { margin-left: auto; order: 9; }
.tw-ghead .tw-gcount { order: 10; }
/* Only ONE auto margin may claim the free space. An existing rule gives it to
   several children at once, and flexbox then splits the gap between them —
   which is why the log button floated to a different spot on each row rather
   than sitting against the total. The log takes the space; everything after
   it keeps a fixed gap. */
.tw-ghead .tw-gcount { margin-left: 8px !important; }
.tw-ghead .tw-log { margin-left: auto !important; }
/* When a P/L chip is present it already carries the auto margin, so the log
   must NOT claim a second one — two auto margins split the free space and both
   the chip and the log drift to a different x on each row. With a chip, the log
   just follows it at a fixed gap. */
.tw-ghead .tw-pnl ~ .tw-log { margin-left: 8px !important; }
/*
 * The count reserves a fixed column so the log button lands at the same x on
 * every row. Pinning the button beside the total was not enough on its own:
 * the total is what varies: a wallet on 9 notifications and one on 127 are two
 * characters different, so the button still sat two characters apart between
 * Bobo and Jojo. Reserved width plus tabular figures makes the whole right
 * edge line up. 3ch covers up to 999; capN() caps the text before it grows.
 */
.tw-ghead .tw-gcount {
  min-width: 4ch; text-align: right;
  font-variant-numeric: tabular-nums;
}

/*
 * Airdrop machines on the board: visible, red, and inert.
 *
 * Hiding them would make the board look like it had missed a busy collection
 * and send the reader to OpenSea to find out. Showing the row with a warning
 * is the honest version — it tells the person what is happening and lets them
 * decide, rather than deciding silently on their behalf.
 */
tr.is-airdrop, tr.is-airdrop td { color: var(--bad); }
tr.is-airdrop {
  cursor: not-allowed;
  background: color-mix(in srgb, var(--bad) 7%, transparent);
}
tr.is-airdrop:hover { background: color-mix(in srgb, var(--bad) 12%, transparent); }
tr.is-airdrop .coll .nm div:first-child::after {
  content: ' ⚠ airdropping';
  font-size: 10px; font-weight: 700; letter-spacing: .04em;
  color: var(--bad); white-space: nowrap;
}
tr.is-airdrop .badge { opacity: .55; }
tr.is-airdrop img, tr.is-airdrop .thumb-ph { filter: grayscale(1); opacity: .5; }

/* The same treatment for the phone card — red, inert, greyed art, and a warning
   after the name so the wash collection is unmistakable without opening it. */
.cd.is-airdrop {
  cursor: not-allowed;
  border-color: color-mix(in srgb, var(--bad) 35%, var(--line));
  background: color-mix(in srgb, var(--bad) 7%, var(--bg-2));
}
.cd.is-airdrop .cd-name { color: var(--bad); }
.cd.is-airdrop .cd-art img, .cd.is-airdrop .cd-art.thumb-ph,
.cd.is-airdrop .cd-art { filter: grayscale(1); opacity: .55; }
.cd.is-airdrop .cd-velo, .cd.is-airdrop .cd-stat b { color: var(--bad); opacity: .8; }
/* The social links stay live even on a flagged card — checking a scam's X is
   fair game — so they are not dimmed into the warning. */
.cd.is-airdrop .cd-soc { color: var(--dim); }

/* PRICE CUT — a mid-mint discount. Amber, NOT the crimson of an airdrop, so the
   two warnings never read as the same thing, and the row STILL opens (only
   airdrop blocks). Desktop row and phone card share the treatment. */
tr.is-pricecut, tr.is-pricecut td { color: var(--warn); }
tr.is-pricecut { background: color-mix(in srgb, var(--warn) 8%, transparent); }
tr.is-pricecut:hover { background: color-mix(in srgb, var(--warn) 13%, transparent); }
tr.is-pricecut .coll .nm div:first-child::after {
  content: ' ⚠ price cut'; font-size: 10px; font-weight: 700;
  letter-spacing: .04em; color: var(--warn); white-space: nowrap;
}
.cd.is-pricecut {
  border-color: color-mix(in srgb, var(--warn) 40%, var(--line));
  background: color-mix(in srgb, var(--warn) 7%, var(--bg-2));
}
.cd.is-pricecut .cd-name { color: var(--warn); }

/* Price RAISE — the bait-and-switch (free→paid, or a squeeze on the last
   tokens). Its own red, DARKER than the airdrop --bad, because the message is
   "stop, the price you saw is not the price you pay" — heavier than a cut and
   distinct from an airdrop, but the card stays live and openable to research. */
tr.is-priceraise, tr.is-priceraise td { color: var(--raise); }
tr.is-priceraise { background: color-mix(in srgb, var(--raise) 9%, transparent); }
tr.is-priceraise:hover { background: color-mix(in srgb, var(--raise) 14%, transparent); }
tr.is-priceraise .coll .nm div:first-child::after {
  content: ' ⚠ price raised'; font-size: 10px; font-weight: 700;
  letter-spacing: .04em; color: var(--raise); white-space: nowrap;
}
.cd.is-priceraise {
  border-color: color-mix(in srgb, var(--raise) 45%, var(--line));
  background: color-mix(in srgb, var(--raise) 8%, var(--bg-2));
}
.cd.is-priceraise .cd-name { color: var(--raise); }

/*
 * The card warning BADGE. The row shows its warning as a ::after on the name,
 * but a phone card truncates the name (ellipsis + nowrap), which clipped the
 * "⚠ price raised" tag off the right edge — invisible exactly when it matters.
 * So the card carries a real, non-truncating badge on the status line instead.
 */
.cd-warn {
  font-size: 10px; font-weight: 800; letter-spacing: .03em;
  white-space: nowrap; flex: none; margin-right: 7px;
  padding: 1px 6px; border-radius: 5px; line-height: 1.5;
}
.cd-warn.is-airdrop   { color: var(--bad);   background: color-mix(in srgb, var(--bad) 15%, transparent); }
.cd-warn.is-pricecut  { color: var(--warn);  background: color-mix(in srgb, var(--warn) 16%, transparent); }
.cd-warn.is-priceraise{ color: var(--raise); background: color-mix(in srgb, var(--raise) 16%, transparent); }

/* ── holder sign-in + device-link (sidebar top) ────────────────────────────── */
.authbox { margin: 0 0 14px; }

/* Signed-OUT: a quiet, tappable row — a mark, a two-line label, a go arrow. */
/* ── Desktop account chip + popover ──────────────────────────────────────────
   The holder card off the page and behind a 30px avatar: identity gradient
   under the holder's own tree, a live-presence dot, and a popover that IS the
   card (the #authbox node moves inside), dressed as one clean sheet — border,
   soft two-layer shadow, arrow notch, 140ms rise. Hidden on phones, where the
   filters drawer keeps hosting the card. */
/* the wrapper is the anchor: the popover positions against THIS 34px box, so
   the arrow notch lands under the avatar — anchored to .barend it drifted to
   whatever sat at the bar's right edge (the theme switcher). */
.acct-wrap { position: relative; display: inline-flex; }
.acct {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0; border-radius: 50%; cursor: pointer;
  background: transparent; border: 0;
}
.acct-av {
  width: 30px; height: 30px; border-radius: 50%; display: inline-flex;
  align-items: center; justify-content: center; font-size: 15px; overflow: hidden;
  background-size: cover; background-position: center;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 14%, transparent),
              0 1px 3px color-mix(in srgb, var(--text) 16%, transparent);
  transition: transform .12s ease, box-shadow .12s ease;
}
.acct:hover .acct-av, .acct[aria-expanded="true"] .acct-av {
  transform: scale(1.06);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 14%, transparent),
              0 0 0 3px color-mix(in srgb, var(--accent) 30%, transparent);
}
.acct-av-anon { background: var(--bg-3); }
.acct-dot {
  position: absolute; right: 0; bottom: 1px; width: 9px; height: 9px; border-radius: 50%;
  background: var(--good); border: 2px solid var(--bg);
}
.acct-pop {
  position: absolute; top: calc(100% + 12px); right: -8px; z-index: 60;
  /* 292px left the tier copy on four and five cramped lines. 336 gives it room
     without changing the sheet; the calc() is a floor so the popover can never
     be wider than the window it hangs in. */
  width: min(336px, calc(100vw - 24px)); box-sizing: border-box;
  padding: 6px; border-radius: 14px;
  background: var(--bg); border: 1px solid var(--line);
  box-shadow: 0 4px 10px color-mix(in srgb, var(--text) 10%, transparent),
              0 16px 44px color-mix(in srgb, var(--text) 16%, transparent);
  transform-origin: top right;
  animation: acctPopIn .14s ease;
}
.acct-pop::before {
  content: ''; position: absolute; top: -5px; right: 16px; width: 9px; height: 9px;
  background: var(--bg); border-left: 1px solid var(--line); border-top: 1px solid var(--line);
  transform: rotate(45deg);
}
@keyframes acctPopIn { from { opacity: 0; transform: translateY(-4px) scale(.97); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .acct-pop { animation: none; } }
/* inside the popover the card sheds its own chrome — the popover IS the card */
.acct-pop .auth-card, .acct-pop .auth-login { border: 0; box-shadow: none; background: transparent; }
.acct-pop .authbox { margin: 0; }
/* the bar is the desktop home; phones keep the drawer card and lose the chip */
@media (max-width: 899px) { .acct, .acct-pop { display: none; } }

.auth-login {
  display: flex; align-items: center; gap: 10px; padding: 10px 12px;
  border-radius: 11px; text-decoration: none; color: var(--text);
  background: var(--bg-2); border: 1px solid var(--line);
  transition: border-color .14s ease, background .14s ease;
}
.auth-login:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); background: var(--bg-3); }
.auth-login-ic {
  width: 30px; height: 30px; flex: none; display: grid; place-items: center; font-size: 16px;
  border-radius: 9px; background: color-mix(in srgb, var(--accent) 16%, var(--bg-3));
}
.auth-login-tx { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
.auth-login-tx b { font-size: 13px; font-weight: 650; }
.auth-login-tx i { font-size: 11px; color: var(--dim); font-style: normal; }
.auth-login-go { margin-left: auto; color: var(--dim); font-size: 16px; }

/* Signed-IN: an account card. Avatar with an identity gradient, address in
   mono, a live "holder" tag, then the device-link action — everything inset
   from the panel edges so nothing reads as a raw full-bleed control. */
.auth-card {
  padding: 13px; border-radius: 12px;
  background: var(--bg-2); border: 1px solid var(--line);
  box-shadow: 0 1px 2px color-mix(in srgb, var(--text) 6%, transparent);
}
.auth-id { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.auth-av {
  width: 34px; height: 34px; flex: none; border-radius: 50%;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 12%, transparent),
              0 1px 3px color-mix(in srgb, var(--text) 14%, transparent);
}
.auth-idtx { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.auth-addr { font-family: var(--mono); font-size: 13px; font-weight: 600; letter-spacing: .01em; }
.auth-tag { display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--dim); }
.auth-dot {
  width: 6px; height: 6px; border-radius: 50%; background: var(--good);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--good) 22%, transparent);
}
.auth-btn {
  width: 100%; display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 9px 14px; border-radius: 9px; cursor: pointer;
  font-size: 13px; font-weight: 650; letter-spacing: .01em;
  color: var(--accent-strong);
  background: color-mix(in srgb, var(--accent) 15%, var(--bg-3));
  border: 1px solid color-mix(in srgb, var(--accent) 42%, var(--line));
  transition: background .14s ease, border-color .14s ease, transform .1s ease;
}
.auth-btn:hover { background: color-mix(in srgb, var(--accent) 24%, var(--bg-3));
  border-color: color-mix(in srgb, var(--accent) 60%, var(--line)); }
.auth-btn:active { transform: translateY(1px); }
.auth-btn.is-busy { opacity: .6; pointer-events: none; }
.auth-foot { font-size: 11px; color: var(--dim); margin: 10px 0 0; line-height: 1.4; }

/* The link reveal — a tidy field + copy, not a wall of text. */
.auth-out:not(:empty) { margin-top: 11px; }
.alp {
  padding: 10px; border-radius: 9px;
  background: var(--bg-3); border: 1px solid var(--line);
}
.alp-lbl { font-size: 11px; color: var(--muted); }
.alp-row { display: flex; gap: 6px; align-items: center; margin: 7px 0 6px; }
.alp-url {
  flex: 1 1 auto; min-width: 0; padding: 7px 9px; border-radius: 7px;
  font-family: var(--mono); font-size: 11px; color: var(--accent-2);
  background: var(--bg); border: 1px solid var(--line);
}
.alp-url:focus { outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent); outline-offset: -1px; }
.alp-copy {
  flex: none; padding: 7px 13px; border-radius: 7px; cursor: pointer;
  font-size: 12px; font-weight: 650; color: #fff; border: 0;
  background: var(--accent-strong);
}
.alp-copy:hover { filter: brightness(1.07); }
.alp-exp { font-size: 10px; color: var(--dim); }
.alp-err { font-size: 12px; color: var(--bad); padding: 4px 0; }
.alp-wait { padding: 4px 0; }

/* Device count under the card — a row of slot dots, the count, and a Manage
   affordance. It is a button (opens the devices sheet) but reads as a caption. */
.auth-devices {
  width: 100%; margin: 11px 0 0; padding: 8px 2px 0;
  display: flex; align-items: center; justify-content: space-between;
  background: none; border: 0; border-top: 1px solid var(--line);
  cursor: pointer; font: inherit; color: var(--dim);
}
.ad-count { display: inline-flex; align-items: center; gap: 7px; font-size: 11.5px; }
.ad-dots { display: inline-flex; gap: 3px; }
.ad-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: color-mix(in srgb, var(--text) 18%, transparent);
}
.ad-dot.on { background: var(--good); box-shadow: 0 0 0 2px color-mix(in srgb, var(--good) 20%, transparent); }
.ad-manage {
  font-size: 11.5px; font-weight: 640; color: var(--accent-strong);
  padding: 2px 0; border-bottom: 1px solid transparent;
}
.auth-devices:hover .ad-manage { border-bottom-color: color-mix(in srgb, var(--accent) 55%, transparent); }
/* Phone-alerts toggle: same line treatment as the devices row, stacked under it
   without a second divider; green when it's on. */
.auth-devices + .auth-push { margin-top: 0; padding-top: 8px; border-top: 0; }
.auth-push .ad-count svg { flex: none; }
.auth-push.is-on .ad-count { color: var(--good); }

/* The top-of-board "enable phone alerts" prompt — the discoverable way in, since
   iOS only asks on a tap and the account-card toggle hides in the filters panel. */
.push-prompt {
  display: flex; align-items: center; gap: 10px; margin: 10px 16px 0; padding: 10px 14px;
  border-radius: 12px; font-size: 13px; color: var(--text);
  background: color-mix(in srgb, var(--accent) 12%, var(--bg-2));
  border: 1px solid color-mix(in srgb, var(--accent) 34%, var(--line));
}
.push-prompt .pp-ic { font-size: 16px; flex: none; }
.push-prompt .pp-tx { flex: 1 1 auto; min-width: 0; line-height: 1.35; }
.push-prompt .pp-go {
  flex: none; padding: 7px 14px; border-radius: 9px; cursor: pointer; border: 0;
  font-size: 13px; font-weight: 650; color: #fff; background: var(--accent-strong);
}
.push-prompt .pp-go:hover { filter: brightness(1.08); }
.push-prompt .pp-go:disabled { opacity: .6; }
.push-prompt .pp-x { flex: none; width: 26px; height: 26px; border-radius: 7px; cursor: pointer; background: none; border: 0; color: var(--muted); font-size: 13px; }
.push-prompt .pp-x:hover { color: var(--text); }

/* ── devices sheet (close one to add another) ──────────────────────────────── */
.auth-modal { position: fixed; inset: 0; z-index: 120; display: grid; place-items: center; padding: 20px; }
.am-scrim { position: absolute; inset: 0; background: rgba(9, 11, 16, .5); backdrop-filter: blur(3px); }
.am-sheet {
  position: relative; width: 100%; max-width: 340px;
  padding: 16px; border-radius: 16px;
  background: var(--bg-2); border: 1px solid var(--line);
  box-shadow: 0 18px 50px rgba(0, 0, 0, .35), 0 2px 8px rgba(0, 0, 0, .18);
  animation: amrise .18s cubic-bezier(.3, .8, .4, 1);
}
.am-loading { display: grid; place-items: center; min-height: 90px; color: var(--dim); font-size: 13px; }
@keyframes amrise { from { opacity: 0; transform: translateY(10px) scale(.985); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .am-sheet { animation: none; } }
.am-head { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 12px; }
.am-ttl { display: flex; flex-direction: column; gap: 2px; }
.am-ttl b { font-size: 15px; font-weight: 680; letter-spacing: -.01em; }
.am-sub { font-size: 11.5px; color: var(--dim); }
.am-x {
  margin-left: auto; flex: none; width: 30px; height: 30px; border-radius: 8px;
  display: grid; place-items: center; cursor: pointer;
  color: var(--muted); background: var(--bg-3); border: 1px solid var(--line); font-size: 13px;
}
.am-x:hover { color: var(--text); border-color: color-mix(in srgb, var(--text) 22%, var(--line)); }
.am-note {
  font-size: 12px; line-height: 1.45; color: var(--accent-strong);
  padding: 9px 11px; margin-bottom: 11px; border-radius: 10px;
  background: color-mix(in srgb, var(--accent) 12%, var(--bg-3));
  border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--line));
}
.am-list { display: flex; flex-direction: column; gap: 7px; }
.am-empty { font-size: 12px; color: var(--dim); padding: 6px 2px; }
.dev-row {
  display: flex; align-items: center; gap: 11px; padding: 10px 11px;
  border-radius: 11px; background: var(--bg-3); border: 1px solid var(--line);
}
.dev-row.is-here { border-color: color-mix(in srgb, var(--good) 34%, var(--line)); background: color-mix(in srgb, var(--good) 7%, var(--bg-3)); }
.dev-ic { flex: none; width: 30px; height: 30px; border-radius: 8px; display: grid; place-items: center;
  color: var(--muted); background: var(--bg); box-shadow: inset 0 0 0 1px var(--line); }
.dev-tx { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.dev-tx b { font-size: 13px; font-weight: 620; display: flex; align-items: center; gap: 7px; }
.dev-tx i { font-size: 10.5px; color: var(--dim); font-style: normal; }
.dev-here {
  font-size: 9.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--good); padding: 1px 6px; border-radius: 999px;
  background: color-mix(in srgb, var(--good) 16%, transparent);
}
/* The primary/home session — the wallet login that never dies. Accent-tinted so
   it reads as the anchor, and it carries no close button. */
.dev-main {
  font-size: 9.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--accent-strong); padding: 1px 6px; border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 17%, transparent);
}
.dev-row.is-primary { border-color: color-mix(in srgb, var(--accent) 32%, var(--line)); background: color-mix(in srgb, var(--accent) 7%, var(--bg-3)); }
.dev-row.is-primary .dev-ic { color: var(--accent-strong); }
.dev-x {
  margin-left: auto; flex: none; width: 30px; height: 30px; border-radius: 8px;
  display: grid; place-items: center; cursor: pointer; font-size: 12px;
  color: var(--muted); background: none; border: 1px solid var(--line);
  transition: color .13s ease, border-color .13s ease, background .13s ease;
}
.dev-x:hover { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 45%, var(--line)); background: color-mix(in srgb, var(--bad) 10%, transparent); }
.dev-x.is-busy { opacity: .5; pointer-events: none; }
.am-add {
  width: 100%; margin-top: 12px; padding: 10px 14px; border-radius: 10px;
  display: inline-flex; align-items: center; justify-content: center; gap: 7px; cursor: pointer;
  font-size: 13px; font-weight: 650; color: var(--accent-strong);
  background: color-mix(in srgb, var(--accent) 15%, var(--bg-3));
  border: 1px solid color-mix(in srgb, var(--accent) 42%, var(--line));
}
.am-add:hover { background: color-mix(in srgb, var(--accent) 24%, var(--bg-3)); }
.am-out:not(:empty) { margin-top: 12px; }

/* ── the holders' room — a tab inside the AI-chat tray ────────────────────── */
/* The tab bar is the tray's title row; it reuses .bell-head's sticky frame. */
.chats-tabs { gap: 4px; padding: 8px 10px; }
.chats-tab {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 12px; border-radius: 9px; cursor: pointer;
  font-size: 12.5px; font-weight: 640; color: var(--muted);
  background: none; border: 1px solid transparent;
}
.chats-tab:hover { color: var(--text); }
.chats-tab.on { color: var(--text); background: var(--bg-3); border-color: var(--line); }
.chats-tab i { font-style: normal; font-size: 10.5px; color: var(--dim); font-weight: 600; }
.chats-tab .hc-dot {
  min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px;
  display: inline-grid; place-items: center; color: #fff; font-size: 10px; font-weight: 700;
  background: var(--accent-strong);
}
/* Holders mode turns the tray into a column of fixed height, so the message list
   scrolls inside and the composer pins to the bottom (rather than the whole
   tray scrolling as it does for the AI list). */
/* The Holders room is a MODAL popup opened from the tray tab (see openHolderPopup
   in app.js) — its own scrim, so an outside tap or an iOS keyboard/zoom shift
   can't slam it shut the way the tray dropdown did. */
.hc-modal { position: fixed; inset: 0; z-index: 90; display: grid; place-items: center; padding: 20px; }
.hc-modal[hidden] { display: none; }
.hc-scrim { position: absolute; inset: 0; background: rgba(9, 11, 16, .5); backdrop-filter: blur(3px); }
.hc-dialog {
  position: relative; z-index: 1; width: 100%; max-width: 440px; height: min(78vh, 620px);
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--bg); border: 1px solid var(--line); border-radius: 16px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, .4);
  animation: hcpop .18s cubic-bezier(.3, .8, .4, 1);
}
@keyframes hcpop { from { opacity: .4; transform: translateY(10px) scale(.985); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .hc-dialog { animation: none; } }
.hc-bar {
  display: flex; align-items: center; gap: 10px; padding: 14px 16px; flex: none;
  border-bottom: 1px solid var(--line); background: var(--bg-2);
}
.hc-ttl { display: flex; flex-direction: column; gap: 1px; line-height: 1.2; }
.hc-ttl b { font-size: 15px; font-weight: 680; letter-spacing: -.01em; }
.hc-ttl span { font-size: 11px; color: var(--dim); }
.hc-x {
  margin-left: auto; width: 32px; height: 32px; border-radius: 8px; flex: none;
  display: grid; place-items: center; cursor: pointer; font-size: 13px;
  color: var(--muted); background: var(--bg-3); border: 1px solid var(--line);
}
.hc-x:hover { color: var(--text); }
/* Phone: the room takes the whole screen so the keyboard has room. Both edges
   pinned (not a vw/vh size) keeps it exact under the root zoom, and the bar
   clears the notch on a home-screen PWA. */
@media (max-width: 640px) {
  .hc-modal { padding: 0; place-items: stretch; }
  .hc-dialog { max-width: none; width: 100%; height: 100%; border: 0; border-radius: 0; animation: none; }
  .hc-bar { padding-top: calc(14px + env(safe-area-inset-top, 0)); }
}

/* Dead now that the room is a popup, kept only so an old cached app.js can't
   render an unstyled inline list; safe to remove once that can't happen. */
.chats-panel.hc-mode {
  display: flex; flex-direction: column; overflow: hidden;
  height: min(70vh, 560px); padding: 0;
}
.chats-panel.hc-mode .bell-head { flex: none; }

.hc-list {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column; gap: 10px; padding: 16px 14px;
}
.hc-note { margin: auto; color: var(--dim); font-size: 13px; text-align: center; padding: 20px; }
.hc-note.hc-err { margin: 0; color: var(--bad); }

.hc-msg { display: flex; align-items: flex-end; gap: 8px; max-width: 88%; }
.hc-msg.grouped { margin-top: -6px; }
.hc-av {
  width: 28px; height: 28px; flex: none; border-radius: 50%; align-self: flex-start;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 12%, transparent);
}
.hc-msg.grouped .hc-av { visibility: hidden; height: 0; align-self: flex-end; }
.hc-bubble {
  padding: 7px 11px 8px; border-radius: 4px 13px 13px 13px; min-width: 0;
  background: var(--bg-2); border: 1px solid var(--line);
}
.hc-mhead { display: flex; align-items: baseline; gap: 8px; margin-bottom: 2px; }
.hc-mhead b { font-family: var(--mono); font-size: 11.5px; font-weight: 650; color: var(--accent-strong); }
.hc-mhead i { font-size: 10px; color: var(--dim); font-style: normal; }
.hc-text { font-size: 13.5px; line-height: 1.4; color: var(--text); overflow-wrap: anywhere; white-space: pre-wrap; }

/* Own messages: mirror to the right, accent-tinted. The holder's own Fruit Tree
   rides on the right so they see their own face in the room, same as everyone
   else's — hiding it was the reason "I don't see my pfp when I message". */
.hc-msg.mine { margin-left: auto; flex-direction: row-reverse; }
.hc-msg.mine .hc-bubble {
  border-radius: 13px 4px 13px 13px;
  background: color-mix(in srgb, var(--accent) 15%, var(--bg-2));
  border-color: color-mix(in srgb, var(--accent) 32%, var(--line));
}
.hc-msg.mine .hc-mhead { flex-direction: row-reverse; }

.hc-compose {
  display: flex; gap: 8px; padding: 12px 14px calc(12px + env(safe-area-inset-bottom, 0));
  border-top: 1px solid var(--line); background: var(--bg-2);
}
.hc-input {
  flex: 1 1 auto; min-width: 0; padding: 11px 13px; border-radius: 11px;
  /* 16px is the threshold below which iOS auto-zooms a focused field — the whole
     reason the chat needed to leave the tray. On a phone the root is zoomed to
     0.9, so 18px there renders ~16.2px, still at/over the threshold. */
  font-size: 16px; color: var(--text);
  background: var(--bg); border: 1px solid var(--line);
}
@media (max-width: 640px) { .hc-input { font-size: 18px; } }
.hc-input:focus { outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent); outline-offset: -1px; }
.hc-send {
  flex: none; width: 42px; border-radius: 11px; cursor: pointer;
  display: grid; place-items: center; color: #fff; border: 0;
  background: var(--accent-strong);
}
.hc-send:hover { filter: brightness(1.08); }
.hc-send:active { transform: translateY(1px); }

/* dividers equidistant around the sort control (public mobile) */
.sortbar.pub-viewbar { margin-bottom: 0; }
#drawer .dhead .links { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
#drawer .dhead .links::-webkit-scrollbar { display: none; }
#drawer .dhead .links a { white-space: nowrap; flex: none; }
