/* Coinyapper — one stylesheet, no build step.
 *
 * Cache-busted by ?v={{ css_version }} (md5 of this file, see app/templates.py),
 * so it can be served with a long max-age and still update instantly.
 *
 * Light and dark are both defined here via prefers-color-scheme. There is no
 * theme toggle: it would need either a cookie read on every render or a flash of
 * the wrong theme on first paint, and neither is worth it yet.
 *
 * The look, stated once so it does not drift. This is a site about assets people
 * lose money on, and the design's whole job is to be *believed*. So: neutral
 * surfaces, hairline borders, no gradients, nothing that looks like a trading
 * terminal pretending to be exciting. Every figure a reader will compare against
 * another figure is set in a tabular mono face, because a column of prices is a
 * table of claims and should read like a document.
 *
 * **The reactions are the one exception, and it is an exception about subject
 * matter rather than a relaxation.** They hold no figures — they are people
 * arriving — so nothing there can be disbelieved on the strength of looking
 * exciting, and being visibly alive is most of what they are for. So the rooms
 * get the movement and the glow (--flash, --glow) and nothing else on the site
 * does: not a price, not a change, not a market cap, not a counter. If a rule
 * here has to bend for something carrying a number, the answer is no.
 *
 * Colour is spent on four things and nothing else: what you can click, whether
 * a number went up or down, the emoji — which are the only genuinely colourful
 * thing on the page and should stay that way — and the mark, which is a boy
 * mid-yell in a cap whose head is the coin, and is therefore made of gold and
 * green rather than of anything else here. That green is a lime, held two hue
 * families away from --up's blue-green on purpose: green already means a number
 * went up, and the only reason the mark may borrow the family at all is that it
 * is the one element on this site with no figure anywhere near it. The --mark-*
 * tokens and --beak, his tongue, appear in the logo and nowhere else. A second
 * use of any of them makes the mark's palette into a page accent, and then the
 * page has two accents fighting; spend --accent.
 *
 * The up/down pair is green/red because that convention is universal in this
 * subject and fighting it would cost comprehension for no gain — but both are
 * desaturated well below the usual, and neither is ever the *only* carrier of
 * meaning: every percentage is signed with a real + or −, so the sign survives
 * for a reader who cannot separate the hues.
 */

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/static/fonts/inter-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/static/fonts/inter-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url('/static/fonts/jetbrains-mono-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url('/static/fonts/jetbrains-mono-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* --- tokens -------------------------------------------------------------- */

:root {
  --bg: #fbfbfa;
  --surface: #ffffff;
  --surface-2: #f4f4f2;
  --text: #16181d;
  --text-dim: #5d6470;
  --text-faint: #8b929e;
  --line: #e3e4e2;
  --line-strong: #cbcdc9;
  --accent: #1f6feb;
  --accent-dim: #dbe7fb;
  --beak: #f0872b;
  /* The mark, and nowhere else. It is a boy mid-yell, so it is not made of the page's
     palette and cannot be: the face IS a coin, so it is gold, and the teeth and
     eye-glints are the only light in it. Naming them as tokens rather than
     hard-coding them into base.html is only so the nav copy and the favicon can
     drift apart per theme without a second SVG. */
  --mark-coin: #f6b73c;
  --mark-coin-edge: #dd9422;
  --mark-light: #fdf3de;
  --mark-ink: #2b2016;
  /* Green is the one hue this page had already given a job: --up. The cap gets
     it anyway because the logo is the only element here with no figure beside
     it — nothing next to the mark can be misread as "this went up". The lime is
     what keeps that true, so keep the distance if it is ever retuned: a cap
     drifting toward --up's #16794a is the bug, not a shade preference. */
  --mark-cap: #52a02a;
  --mark-cap-band: #3c7a1c;
  /* Spent only on a reaction as it lands — see the note at the top of this
     file on where colour goes. Every figure on the page stays flat. */
  --flash: rgba(31, 111, 235, .16);
  --glow: rgba(31, 111, 235, .30);
  --up: #16794a;
  --down: #b23132;
  --danger: #b23132;
  --radius: 10px;
  /* The sticky header's own height. It is a token because two other things have
     to agree with it: `nav` sets its `min-height` from this, and every sticky
     offset below the header is measured from it. The room rail sticks *under* a
     header that also sticks, and getting that offset wrong hides the top of the
     rail behind it — which, now that the feed's newest reaction is its first
     row, is the one row somebody came to the front page to see. */
  --nav-h: 60px;
  --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --wrap: 1120px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0e1013;
    --surface: #16191e;
    --surface-2: #1c2027;
    --text: #e8eaed;
    --text-dim: #a0a7b4;
    --text-faint: #767d8a;
    --line: #262b33;
    --line-strong: #363d47;
    --accent: #62a0ff;
    --accent-dim: #17243a;
    --beak: #ffa64d;
    --mark-coin: #ffc24d;
    --mark-coin-edge: #e8a02c;
    --mark-light: #fff5e2;
    --mark-ink: #23190e;
    --mark-cap: #6fbe3c;
    --mark-cap-band: #55992c;
    --flash: rgba(98, 160, 255, .18);
    --glow: rgba(98, 160, 255, .45);
    --up: #4cc38a;
    --down: #ef6f6f;
    --danger: #ef6f6f;
  }
}

*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

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

.mono, .mono-input, code { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.muted { color: var(--text-dim); }
.up { color: var(--up); }
.down { color: var(--down); }
.flat { color: var(--text-dim); }

/* --- nav ----------------------------------------------------------------- */

header {
  border-bottom: 1px solid var(--line);
  background: var(--surface);
  position: sticky; top: 0; z-index: 40;
}

nav {
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 0 20px;
  display: flex; align-items: center; gap: 20px;
  min-height: var(--nav-h);
}

.logo { display: flex; align-items: center; gap: 9px; color: var(--text); font-weight: 700; }
.logo:hover { text-decoration: none; }
/* The mark brings its own ground — a filled circle — so this rule only reserves
 * the box. Anything set here that the SVG also sets (a background, a radius) is
 * a second mark behind the first one. */
.logo-mark { width: 1.85rem; height: 1.85rem; flex: none; }
.logo-mark svg { display: block; width: 100%; height: 100%; }
.logo-text { letter-spacing: -.015em; }

.nav-search { position: relative; flex: 1; max-width: 340px; }
.nav-search input {
  width: 100%; padding: 7px 12px;
  border: 1px solid var(--line-strong); border-radius: 8px;
  background: var(--bg); color: var(--text); font: inherit; font-size: .9rem;
}
.nav-search input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }

.suggestions {
  position: absolute; top: calc(100% + 5px); left: 0; right: 0;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,.10);
  overflow: hidden; z-index: 50;
}
.suggestion { display: flex; align-items: center; gap: 10px; padding: 8px 12px; color: var(--text); font-size: .9rem; }
.suggestion:hover { background: var(--surface-2); text-decoration: none; }
.suggestion .mono { font-weight: 600; min-width: 62px; }
.suggestion-name { color: var(--text-dim); flex: 1; }

#nav-menu { list-style: none; display: flex; align-items: center; gap: 18px; margin: 0 0 0 auto; padding: 0; }
#nav-menu a { color: var(--text-dim); font-size: .92rem; font-weight: 500; }
#nav-menu a:hover, #nav-menu a.active { color: var(--text); text-decoration: none; }

.nav-toggle { display: none; background: none; border: 0; cursor: pointer; padding: 8px; }
.nav-toggle span { display: block; width: 20px; height: 2px; background: var(--text); margin: 4px 0; }

.nav-dd { position: relative; }
.nav-avatar-btn { background: none; border: 0; cursor: pointer; padding: 0; }
.nav-avatar {
  width: 32px; height: 32px; border-radius: 50%;
  background: var(--surface-2); border: 1px solid var(--line-strong);
  display: grid; place-items: center; font-size: .75rem; font-weight: 700; color: var(--text-dim);
}
.nav-dd-panel {
  display: none; position: absolute; right: 0; top: calc(100% + 9px);
  min-width: 220px; background: var(--surface);
  border: 1px solid var(--line); border-radius: 10px;
  box-shadow: 0 10px 30px rgba(0,0,0,.12); padding: 6px; z-index: 50;
}
.nav-dd.open .nav-dd-panel { display: block; }
.nav-dd-head { padding: 8px 10px; border-bottom: 1px solid var(--line); margin-bottom: 5px; }
.nav-dd-head strong { display: block; font-size: .9rem; }
.nav-dd-head span { display: block; font-size: .78rem; color: var(--text-faint); overflow: hidden; text-overflow: ellipsis; }
.nav-dd-panel a, .nav-dd-form button {
  display: block; width: 100%; text-align: left;
  padding: 7px 10px; border-radius: 6px; border: 0; background: none;
  color: var(--text-dim); font: inherit; font-size: .9rem; cursor: pointer;
}
.nav-dd-panel a:hover, .nav-dd-form button:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }

.nav-inline-form { display: inline; }
.link-button { background: none; border: 0; color: var(--accent); font: inherit; font-size: .92rem; cursor: pointer; padding: 0; }

@media (max-width: 800px) {
  .nav-toggle { display: block; order: 3; margin-left: auto; }
  .nav-search { order: 4; flex-basis: 100%; max-width: none; margin-bottom: 12px; }
  #nav-menu {
    order: 5; flex-basis: 100%; margin: 0 0 12px; display: none;
    flex-direction: column; align-items: flex-start; gap: 12px;
  }
  nav { flex-wrap: wrap; padding-top: 12px; }
  nav.menu-open #nav-menu { display: flex; }
  .nav-dd-panel { position: static; box-shadow: none; border: 0; padding: 0; min-width: 0; }
}

/* --- layout -------------------------------------------------------------- */

main { max-width: var(--wrap); margin: 0 auto; padding: 32px 20px 64px; }
section { margin-bottom: 44px; }
.narrow { max-width: 680px; }

h1 { font-size: 1.85rem; line-height: 1.25; letter-spacing: -.02em; margin: 0 0 8px; }
h2 { font-size: 1.15rem; letter-spacing: -.01em; margin: 0 0 6px; }

.hero { margin-bottom: 40px; }
.hero h1 { font-size: 2.3rem; }
.lede { font-size: 1.05rem; color: var(--text-dim); max-width: 62ch; margin: 0; }

.section-head { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; margin-bottom: 16px; }
.section-head h1, .section-head h2 { margin: 0; }
.section-sub { color: var(--text-dim); font-size: .9rem; margin: 0; flex: 1 1 100%; }
.page-head { display: block; margin-bottom: 24px; }
.page-head .section-sub { max-width: 68ch; margin-top: 6px; }
.section-more { font-size: .85rem; margin-left: auto; }

.notice { background: var(--accent-dim); border: 1px solid var(--accent); color: var(--text); padding: 10px 14px; border-radius: 8px; font-size: .9rem; }
.form-error { background: color-mix(in srgb, var(--danger) 12%, transparent); border: 1px solid var(--danger); color: var(--text); padding: 10px 14px; border-radius: 8px; font-size: .9rem; }

.empty-state { border: 1px dashed var(--line-strong); border-radius: var(--radius); padding: 32px; text-align: center; }
.empty-state h1, .empty-state h2 { margin-bottom: 8px; }
.empty-state p { color: var(--text-dim); margin: 0 auto; max-width: 54ch; }

/* --- coin cards ---------------------------------------------------------- */

.coin-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); }

/* Up today and down today, beside each other once there is width for it. They
   are the same question asked both ways up, and reading one under the other
   meant scrolling past eight rising coins to find out what fell — on a page
   whose subject is what moved today, the two halves of that answer belong in
   one glance.

   `auto-fit`, not `auto-fill`: either section is omitted entirely on a quiet
   day (see the `{% if %}` around each in index.html), and the survivor should
   take the whole width rather than sit in a half-width column beside a hole. */
@media (min-width: 900px) {
  .day-pair { display: grid; gap: 28px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); align-items: start; margin-bottom: 44px; }
  /* The 44px every section carries is vertical rhythm between stacked
     sections; side by side it is the taller column's trailing whitespace, and
     the pair itself now carries it instead. */
  .day-pair > section { margin-bottom: 0; }
}

.coin-card {
  display: block; padding: 14px 16px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); color: var(--text);
}
.coin-card:hover { border-color: var(--line-strong); text-decoration: none; }
/* A written-up coin is marked. A card that looks the same either way teaches a
   reader that the site's own writing is not worth seeking out. */
.coin-card-head { display: flex; align-items: baseline; gap: 8px; }

/* The coin's logo, mirrored locally and served from /icons — never hotlinked
   from the provider. See services/icons.py; the template is
   partials/coin_icon.html.

   `align-self: center` rather than switching the two containers to
   `align-items: center`: the symbol and the name are still baseline-aligned to
   each other, which is what makes a ticker and a name read as one line, and an
   image has no useful baseline of its own — left alone it would sit its bottom
   edge on the text baseline and push the whole row down.

   The box is sized by the `width`/`height` attributes the macro writes, so the
   space is reserved before the image lands and forty of them do not reflow the
   page one at a time. `object-fit: contain` is for the handful of logos that
   are not square; `background` gives a transparent PNG something to sit on in
   dark mode, where a black-on-transparent logo would otherwise vanish. */
.coin-icon {
  flex: 0 0 auto; align-self: center;
  object-fit: contain; border-radius: 50%;
  background: var(--surface-2);
}
/* No logo yet — a letter from the coin's own ticker. Deliberately not a shared
   placeholder glyph: this is drawn on every coin the fetch pass has not reached,
   which early on is most of the catalog, and a grid of identical grey circles
   says less than nothing. Same box as the image, so the two states are
   interchangeable without moving anything. */
.coin-icon-none {
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--mono); font-weight: 600; line-height: 1;
  color: var(--text-faint); border: 1px solid var(--line);
  background: var(--surface-2);
}
.coin-symbol { font-family: var(--mono); font-weight: 600; font-size: .85rem; color: var(--text); }
.coin-symbol.big { font-size: 1rem; color: var(--text-dim); }
.coin-name { color: var(--text-dim); font-size: .88rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.coin-card-line { margin: 6px 0 10px; font-size: .92rem; line-height: 1.45; }
.coin-card-line.muted { font-size: .85rem; }
.coin-card-figures { display: flex; gap: 12px; flex-wrap: wrap; font-family: var(--mono); font-size: .8rem; font-variant-numeric: tabular-nums; }
.coin-card-figures .figure.muted, .coin-card-figures .reactions { color: var(--text-faint); }
/* Bull votes are a count, not a price move, so they get the up colour but not
   the weight of one — a card should not read as though the site is calling a
   coin green. */
.coin-card-figures .figure.bulls { color: var(--up); opacity: .85; }

/* --- coin page ----------------------------------------------------------- */

.coin-page { max-width: 760px; }
.coin-head { margin-bottom: 20px; }
.coin-title { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.byline { color: var(--text-faint); font-size: .85rem; margin: 6px 0 0; }

.market { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 16px; }
.market-figures { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 14px; margin: 0; }
.market-figures div { margin: 0; }
.market-figures dt { font-size: .72rem; text-transform: uppercase; letter-spacing: .05em; color: var(--text-faint); }
.market-figures dd { margin: 2px 0 0; font-size: 1rem; font-weight: 600; }
/* Now only the "no market data yet" line; the per-figure provenance line was
   removed from the coin page by request. */
.market-age { color: var(--text-faint); font-size: .8rem; margin: 14px 0 0; }
.contract { margin: 10px 0 0; font-size: .78rem; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.contract-label { text-transform: uppercase; letter-spacing: .05em; color: var(--text-faint); font-size: .7rem; }
.contract code { color: var(--text-dim); word-break: break-all; }

/* --- the ingested profile -------------------------------------------------
   Replaced the article block. The quote is visually set apart from everything
   else on the page on purpose: it is the only text here this site did not
   write, and a reader has to be able to see that at a glance rather than by
   reading the caption. */
.profile { margin-top: 28px; }
/* The composed summary. Set as body text, not as a quote: this is the site
   speaking, and the blockquote below it is not. */
.profile-summary { font-size: 1.02rem; line-height: 1.7; margin: 0 0 22px; max-width: 62ch; }

.coin-tags { display: flex; gap: 6px; flex-wrap: wrap; margin: 10px 0 0; }
/* Every one of these is a link now — see the note in coin.html. They keep the
   quiet pill they had as inert labels rather than taking the accent: a header
   with four blue pills under the coin's name would compete with the name, and
   these are a classification rather than a call to action. The border moving on
   hover is what says they are clickable. */
.tag { display: inline-block; font-size: .72rem; color: var(--text-dim); background: var(--surface-2); border: 1px solid var(--line); border-radius: 999px; padding: 2px 9px; }
a.tag:hover { border-color: var(--accent); color: var(--text); text-decoration: none; }

.profile-quote { margin: 0 0 24px; border-left: 3px solid var(--line-strong); padding-left: 18px; }
.profile-quote blockquote { margin: 0; color: var(--text-dim); }
.profile-quote figcaption { margin-top: 10px; font-size: .78rem; color: var(--text-faint); }

.profile-supply { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 14px; margin: 0 0 24px; }
.profile-supply div { margin: 0; }
.profile-supply dt { font-size: .72rem; text-transform: uppercase; letter-spacing: .05em; color: var(--text-faint); }
.profile-supply dd { margin: 2px 0 0; font-size: 1rem; font-weight: 600; }

.profile-links h2 { font-size: 1rem; margin: 0 0 10px; }
.profile-links ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px 10px; }
.profile-links li { display: inline-flex; align-items: center; gap: 6px; }
.profile-link { font-size: .88rem; border: 1px solid var(--line); border-radius: 8px; padding: 5px 12px; text-decoration: none; color: var(--text); background: var(--surface); }
.profile-link:hover { border-color: var(--accent); background: var(--accent-dim); }
/* A dead link stays clickable. The claim is "this did not answer when we
   checked", not "we have decided this is gone" — and a reader is entitled to
   go and find out for themselves. */
.profile-link.dead { color: var(--text-faint); border-style: dashed; text-decoration: line-through; }
.link-dead { font-size: .72rem; color: var(--danger); }
.profile-checked { color: var(--text-faint); font-size: .78rem; margin: 12px 0 0; }
/* The supply block's date. Same typography as .profile-checked above — both are
   a caption saying when a borrowed number was read — but its own margins,
   because it sits under a dl that already carries 24px of bottom margin and a
   caption 36px from the figures it dates reads as unrelated to them. */
.profile-supply-age { color: var(--text-faint); font-size: .78rem; margin: -14px 0 24px; }

.profile-pending { border: 1px dashed var(--line-strong); border-radius: var(--radius); padding: 22px; }
.profile-pending h2 { margin-bottom: 8px; font-size: 1.05rem; }
.profile-pending p { color: var(--text-dim); font-size: .92rem; max-width: 58ch; }

/* --- pager ----------------------------------------------------------------
   Real links, one per page, because each one is a URL a crawler can reach and a
   reader can bookmark — which a "load more" button is not. */
.pager { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; margin-top: 28px; justify-content: center; }
.pager-page, .pager-step { font-size: .88rem; padding: 6px 12px; border: 1px solid var(--line); border-radius: 8px; text-decoration: none; color: var(--text-dim); background: var(--surface); }
.pager-page:hover, .pager-step:hover { border-color: var(--accent); color: var(--text); }
.pager-page.current { background: var(--accent-dim); border-color: var(--accent); color: var(--text); font-weight: 600; }
.pager-gap { color: var(--text-faint); padding: 0 2px; }

/* --- prose --------------------------------------------------------------- */

.prose { font-size: 1.02rem; line-height: 1.72; }
.prose h2 { font-size: 1.3rem; margin: 32px 0 10px; }
.prose h3 { font-size: 1.08rem; margin: 24px 0 8px; }
.prose p { margin: 0 0 16px; }
.prose ul, .prose ol { margin: 0 0 16px; padding-left: 22px; }
.prose li { margin-bottom: 6px; }
.prose blockquote {
  margin: 0 0 16px; padding: 2px 0 2px 16px;
  border-left: 3px solid var(--line-strong); color: var(--text-dim);
}
.prose code { background: var(--surface-2); padding: 1px 5px; border-radius: 4px; font-size: .88em; }

/* --- signals ------------------------------------------------------------- */

.signal-bar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 20px 0; }
.signal-bar form { margin: 0; }
.signal-btn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 13px; border: 1px solid var(--line-strong); border-radius: 999px;
  background: var(--surface); color: var(--text-dim);
  font: inherit; font-size: .86rem; cursor: pointer;
}
.signal-btn:hover { border-color: var(--accent); color: var(--text); }
.signal-btn.on { border-color: var(--accent); background: var(--accent-dim); color: var(--text); }
.signal-btn b { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: .8rem; }
/* A drawn bull and bear rather than 🐂 / 🐻 — see the note in signal_bar.html on
   why the animal was never the problem and the glyph was. Coloured with the
   up/down pair, because direction is most of what the mark says; the word beside
   it carries the meaning for anyone who cannot separate the hues.

   `currentColor` on the stroke is what makes the two rules below enough: the
   icon takes the button's colour, including the dim resting colour and the
   brighter `:hover` and `.on` states, so there is no second set of states to
   keep in step. The ledger has no rule and therefore no tint — a watchlist has
   no direction. */
.signal-icon { width: 17px; height: 17px; flex: 0 0 auto; }
.signal-bull .signal-icon { color: var(--up); }
.signal-bear .signal-icon { color: var(--down); }
.signal-flag { margin-left: auto; font-size: .82rem; color: var(--text-faint); }

/* --- the two-column layouts ----------------------------------------------
   Content left, room right, on anything wide enough to spare 340px. The room
   is the half of this site that moves; below a long page it was only ever seen
   by someone who had finished reading.

   One DOM order serves both layouts. On a narrow screen these collapse to a
   flex column — and the home page's rail takes `order: -1` so the feed stays
   above the coin lists, which is the position it is supposed to hold at every
   width. The coin page keeps source order instead: there, the market figures
   and the profile are what the visitor came for and the room follows them. */
.coin-layout, .home-layout { display: flex; flex-direction: column; gap: 28px; }
.home-layout > .room-rail { order: -1; }

@media (min-width: 1040px) {
  .coin-layout, .home-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 340px;
    align-items: start;
    gap: 32px;
  }
  .home-layout > .room-rail { order: 0; }
  /* Pinned, so the room stays on screen for the whole scroll. `align-self:
     start` is what makes sticky work inside a grid — a stretched item is
     already as tall as the row and has nothing to stick within. */
  /* Under the header, not behind it: `--nav-h` plus its bottom hairline plus the
     same 16px of air the rail would have had on its own. `top: 16px` measured
     from the viewport, so the first 45px of the rail sat beneath a header that
     is opaque and `z-index: 40`. On a coin page that hid the heading and the
     tally, which is why it went unnoticed for so long. */
  .room-rail { position: sticky; top: calc(var(--nav-h) + 17px); }
  /* The feed has no height rule here any more, because it has no box: it is a
     plain list of `chat.FEED_LIMIT` rows that ends where it ends. The clamp that
     used to live here existed to keep a *scrolling* box from hanging its bottom
     — where the newest reaction was — off the bottom of a pinned rail. Newest
     first removes the reason: what hangs off a short window now is the oldest
     rows, and the sticky rail keeps the ones that matter on screen for the whole
     scroll. */
  /* The reading column no longer needs its own cap: the grid track is the
     measure now, and 760px inside a 1fr track would leave a ragged gap. */
  .coin-page { max-width: none; }
}

/* --- the rooms -----------------------------------------------------------
   The one part of this site that is allowed to be loud, and the restraint
   everywhere else is what pays for it. Everything holding a *figure* stays
   flat — a price in a box that flashes is a price nobody believes. The
   reactions carry no figures: they are people, arriving, and the emoji are
   already the only colour on the page (see the note at the top of this file),
   so the flash is spent on them and on nothing else.

   Which is also why the room has no frame any more. A bordered, filled panel
   with a bordered, filled box inside it drew a *chat widget* — a thing bolted
   onto the page whose first-class visual feature was its own edge. Two borders
   removed and it reads as part of the page, with the emoji doing the work the
   chrome was doing badly. The scroll region is bounded by a fade rather than a
   line, so nothing draws where the box stops. */

.room .section-head, .feed .section-head { margin-bottom: 10px; }

/* A live indicator on the heading, because the box no longer has an edge to
   say "this moves". Cheap in every sense: one pseudo-element, no markup, and it
   is the only pulsing thing on the site. */
.room .section-head h2, .feed .section-head h2 { display: flex; align-items: center; gap: 9px; }
.room .section-head h2::before, .feed .section-head h2::before {
  content: ""; flex: 0 0 auto;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--up); box-shadow: 0 0 0 0 var(--glow);
  animation: room-live 2.4s ease-out infinite;
}
@keyframes room-live {
  0%   { box-shadow: 0 0 0 0 var(--glow); }
  70%  { box-shadow: 0 0 0 7px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* Where the front page says what it cannot do: there is no picker under the
   feed, so the space one would have taken says where to react instead. A dead
   end is worse than a signpost. */
.feed-note { margin: 12px 0 0; font-size: .82rem; color: var(--text-faint); line-height: 1.5; }

/* The standing tally. Pills without borders: sixteen hairline boxes above a
   frameless strip only move the chrome one row up the panel. */
.chat-summary { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
.chat-tally {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 9px 3px 7px; border-radius: 999px;
  background: var(--surface-2); font-size: 1.15rem; line-height: 1.2;
}
.chat-tally b { font-family: var(--mono); font-size: .74rem; color: var(--text-dim); font-weight: 600; }

/* A vertical feed, newest at the bottom — a chat, not a ticker.
   It used to be a horizontal strip, on the reasoning that emoji have no reading
   order and a vertical list owns the screen. In a 340px rail that trade goes
   the other way: the column has height to spend and no width, and arrivals
   stacking downward is the shape everyone already reads as "live".

   **The mask is what replaces the border.** Rows fade out at the top edge
   instead of being cut off by a line, which says "there is more above" more
   honestly than a frame does and lets the strip sit straight on the page
   background. There is deliberately no fade at the bottom: the newest reaction
   lives there and is the one row that must always be fully legible. */
.chat-strip {
  display: flex; flex-direction: column; gap: 2px;
  /* **Both axes stated, and `hidden` on x is the load-bearing half.**
     `overflow-y: auto` on its own computes x to `auto` too, which makes this a
     scroll container in *both* directions — and a transform contributes to a
     scroll container's overflow region, so the arrival pop could put a
     horizontal scrollbar across the bottom of the room for the length of the
     animation and take it away again. Nothing in a row is meant to scroll
     sideways: the sender's name ellipsises to fit, by design. */
  overflow-x: hidden; overflow-y: auto; overscroll-behavior-y: contain;
  /* And the vertical gutter is reserved whether or not there is a scrollbar, so
     a room crossing the overflow threshold — which is what sending the reaction
     that fills the box does — does not reflow every row by the width of one. */
  scrollbar-gutter: stable;
  /* The bottom padding is not decoration: it is what the arrival keyframe rises
     *through*. A reaction animating in from 10px below its resting place grows
     the scrollable area unless there is padding under it to move inside, and a
     scroll area that grows for 400ms is a scrollbar that blinks. Keep this
     larger than the keyframe's translate. */
  padding: 10px 0 12px; height: 340px;
  scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 34px, #000 100%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 34px, #000 100%);
}
/* Few enough reactions to fit and they sit at the *bottom*, where the scrolled
   state puts them — so a room filling up does not move its newest row from the
   top of the box to the bottom of it. Resolves to 0 the moment the content
   overflows, so it cannot fight the scrolling. */
.chat-strip > *:first-child { margin-top: auto; }

/* The feed is not a box and must not be one. A scrollable panel halfway down the
   front page catches the wheel on the way past and scrolls its own forty rows
   instead of the page — the reader's scroll goes somewhere they did not aim it,
   and `overscroll-behavior: contain` below means it does not chain back out
   either. So: no height, no overflow, no gutter, and no fade, since there is no
   longer an edge for content to be cut off at. Newest at the top, which is what
   makes a list with no scrollbar work at all — see partials/chat_strip.html. */
.feed .chat-strip {
  height: auto; padding: 2px 0 0;
  scrollbar-gutter: auto;
  -webkit-mask-image: none; mask-image: none;
  /* Bounded by the window, and `clip` rather than `auto` or `hidden`.

     The list has to fit beside the coin lists in a rail that sticks under the
     header, because a sticky panel taller than the viewport hangs its bottom
     off the screen *and* runs out of travel inside its grid area early — after
     which it rides up and off the top, taking the newest reactions with it. A
     733px rail in an 820px window ran out after 392px of a 880px scroll, so for
     most of the front page the top row on screen was not the newest one. That is
     what "the newest is not at the top" looked like.

     `clip` is what keeps that from re-introducing the wheel trap: it bounds the
     box without making it a scroll container at all, so there is nothing to
     catch a scroll and no scrollbar to appear. What falls outside is the oldest
     end of a list whose newest row is its first, which is the right thing to
     lose. The subtrahend is the rest of the rail — sticky offset, heading, tally
     and note — measured at 205px plus slack. */
  max-height: calc(100vh - var(--nav-h) - 225px);
  /* And snapped down to a whole number of rows, so the clip lands between two
     reactions instead of through the middle of one. 44px is a row: 42 plus the
     2px gap. Declared after the plain `calc` above, so a browser without
     `round()` drops this line and keeps the unsnapped bound — the fallback is a
     half-row, not a broken layout. */
  max-height: round(down, calc(100vh - var(--nav-h) - 225px), 44px);
  overflow: clip;
}

.chat-item, .chat-ghost {
  display: flex; align-items: center; gap: 10px;
  flex: 0 0 auto; padding: 4px 8px; border-radius: 10px;
}
/* In the feed every row is a link to the coin it came from. Styled as a row
   rather than as a link — an underlined blue line per reaction would turn the
   rail into a wall of chrome — with the hover doing the work of saying so. */
a.chat-item { color: inherit; text-decoration: none; transition: background .12s ease; }
a.chat-item:hover, a.chat-item:focus-visible { background: var(--surface-2); }
/* The emoji is the content. Everything else on the row is a caption for it, so
   it is set several times their size and is the only thing that ever glows. */
.chat-emoji {
  /* `inline-block` is load-bearing, not tidiness: a transform does not apply to
     a non-replaced *inline* element, so the arrival keyframes below animated
     nothing but opacity while this was a bare span — which is why the old pop
     never looked like one. */
  display: inline-block; font-size: 1.85rem; line-height: 1.15;
}
/* The ticker the reaction was sent about, and the reason the feed is worth
   reading at all: an emoji with nothing beside it is a reaction to nothing.
   Given the room the sender's name gives up — `min-width: 0` on the flex child
   is what lets the name ellipsis instead of squeezing this. */
.chat-ticker {
  font-family: var(--mono); font-size: .74rem; font-weight: 600;
  color: var(--text); flex: 0 0 auto;
}
.chat-sender { font-size: .72rem; color: var(--text-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.chat-empty { color: var(--text-faint); font-size: .88rem; padding: 4px 8px; margin: auto 0; }

/* Arrivals. Applied by the hook in base.html to items that were not on the page
   before the swap, and removed again once it has played — so a reaction that
   arrives while you are watching lands with some weight, and the forty
   server-rendered ones on first paint do not all fire at once. */
@keyframes chat-arrive {
  /* 10px, against the strip's 12px of bottom padding — see the note there. */
  from { opacity: 0; transform: translateY(10px) scale(.55); }
  55%  { opacity: 1; transform: translateY(0) scale(1.22); }
  to   { opacity: 1; transform: none; }
}
/* The row behind it, lit for a beat. It ends on `transparent` rather than on a
   surface colour so it leaves nothing behind — the row has no background of its
   own to return to. */
@keyframes chat-flash {
  from { background: var(--flash); }
  to   { background: transparent; }
}
@keyframes chat-glow {
  from { filter: drop-shadow(0 0 12px var(--glow)); }
  to   { filter: none; }
}
.chat-item.is-new { animation: chat-flash .9s ease-out; }
.chat-item.is-new .chat-emoji {
  animation: chat-arrive .42s cubic-bezier(.2, .9, .3, 1.6), chat-glow .9s ease-out;
}

/* Your own reaction, echoed locally the instant you click rather than waiting
   for the next poll to bring it back. Deliberately dimmed: it is not in the
   database yet, and it is replaced by the real row a beat later. Not a
   `.chat-item` — the poll cursor reads the last `.chat-item[data-id]`, and a
   ghost in that list would reset the cursor to 0 and re-send the entire tail. */
.chat-ghost { opacity: .45; }
.chat-ghost .chat-emoji { animation: chat-arrive .34s ease-out; }

@media (prefers-reduced-motion: reduce) {
  .chat-item.is-new, .chat-item.is-new .chat-emoji, .chat-ghost .chat-emoji,
  .room .section-head h2::before, .feed .section-head h2::before { animation: none; }
}

/* The picker. Frameless like the strip: sixteen bordered boxes under a strip
   with no border is the chrome coming back in through the bottom of the panel.
   The hover lift is the affordance the border was carrying, and
   `:focus-visible` is what keeps that honest for a keyboard — a button whose
   only "I am a button" is a mouse hover is not a button for everybody. */
/* The picker reserves a strip at its top for the refusal to appear in, at every
   state, whether or not there is one. That strip is why a refusal no longer
   moves anything: the countdown is positioned into space that was already
   there, the buttons stay exactly where they were, and the panel keeps its
   height. `margin-top` gives back most of what the padding takes, so the gap
   between the reactions and the buttons is what it was. */
.picker { position: relative; margin-top: 12px; }
/* And the strip is reserved only where a refusal can actually land, which is
   exactly where there is an account: `data-me` is set on the signed-in arm and
   nowhere else. Signed out there is nothing to hold space for. */
.picker[data-me] { margin-top: 4px; padding-top: 22px; }
.picker-groups { display: flex; gap: 16px; flex-wrap: wrap; }
.picker-group { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; }
.picker-heading { font-size: .68rem; text-transform: uppercase; letter-spacing: .06em; color: var(--text-faint); margin-right: 2px; }
.emoji-btn {
  background: none; border: 0; border-radius: 12px; cursor: pointer;
  font-size: 1.7rem; line-height: 1; padding: 5px 6px;
  transition: transform .12s cubic-bezier(.2, .9, .3, 1.5), background .12s ease, filter .12s ease;
}
.emoji-btn:hover {
  background: var(--surface-2); transform: translateY(-3px) scale(1.16);
  filter: drop-shadow(0 0 9px var(--glow));
}
.emoji-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; background: var(--surface-2); }
.emoji-btn:active { transform: translateY(0) scale(.92); }
/* Cooling down. The hover rules match a disabled button too — `:hover` does not
   care — so each one has to be put back, or a refused picker still lifts and
   glows under the cursor and reads as clickable. */
.emoji-btn:disabled { opacity: .28; cursor: default; }
.emoji-btn:disabled:hover {
  transform: none; filter: none; background: none;
}
@media (prefers-reduced-motion: reduce) {
  .emoji-btn { transition: background .12s ease; }
  .emoji-btn:hover { transform: none; filter: none; }
}

.picker-signin, .picker-refused { color: var(--text-dim); font-size: .9rem; margin: 0; }
/* `.picker-signin` is a div wrapping either a <p> or a <form> — see the note in
   chat_picker.html on why it cannot be a <p> itself. Collapse the child's margin
   so the two arms sit identically. */
.picker-signin > p { margin: 0; }
/* The refusal, in place of the buttons — the one thing on this panel that
   *replaces* content rather than sitting beside it, which is the whole point:
   for that second there is nothing to click. It was a filled, rounded, padded
   bar, and once the room lost its frame that bar was the only boxed element
   left in the panel — so a refusal read as a widget sliding in rather than as
   an answer to the click. Now it is a line of text where the buttons were.

   `min-height` is one button's worth of box (1.7rem line + 10px of padding),
   so the panel below does not jump up by a row and back down a second later.
   The rest of the jump is inherent: buttons wrap to more rows than one line of
   text, and that is a fair price for a refusal that cannot be clicked. */
.picker-cooldown {
  position: absolute; top: 0; left: 0; right: 0; height: 22px;
  display: flex; align-items: center; gap: 8px;
  font-size: .84rem; color: var(--text-dim);
}
.picker-cooldown strong { font-weight: 600; color: var(--text); }
.picker-count { font-family: var(--mono); color: var(--text-faint); }

/* --- tables -------------------------------------------------------------- */

table.data, table.desk {
  width: 100%; border-collapse: collapse; font-size: .86rem;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  overflow: hidden; margin-bottom: 28px;
}
table.data th, table.desk th {
  text-align: left; font-size: .7rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--text-faint); padding: 10px 12px; border-bottom: 1px solid var(--line);
  font-weight: 600;
}
table.data td, table.desk td { padding: 10px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
table.data tr:last-child td, table.desk tr:last-child td { border-bottom: 0; }

/* Wide tables scroll inside their own container rather than making the page
   scroll sideways. */
.desk-wrap { overflow-x: auto; }

.desk-name { display: block; color: var(--text-dim); font-size: .82rem; }
.desk-identity { display: block; color: var(--text-faint); font-size: .7rem; }
.desk-note { color: var(--text-dim); font-size: .82rem; }
.desk-actions { display: flex; gap: 6px; align-items: flex-start; flex-wrap: wrap; }
.dismiss-form { display: flex; gap: 4px; }
.dismiss-form input { width: 108px; padding: 4px 7px; border: 1px solid var(--line-strong); border-radius: 6px; background: var(--bg); color: var(--text); font: inherit; font-size: .78rem; }
.urgency { font-size: .82rem; color: var(--text); }

.desk-debt { font-size: .95rem; color: var(--text-dim); margin: 0 0 20px; }
.desk-debt strong { font-family: var(--mono); font-size: 1.3rem; color: var(--text); }

.pill { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: .72rem; border: 1px solid var(--line-strong); color: var(--text-dim); }
.pill.published { border-color: var(--up); color: var(--up); }
.pill.draft { border-color: var(--accent); color: var(--accent); }

.stat-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 32px; }
.stat {
  flex: 1 1 130px; background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px 16px;
}
.stat b { display: block; font-family: var(--mono); font-size: 1.5rem; font-variant-numeric: tabular-nums; }
.stat span { font-size: .78rem; color: var(--text-faint); }
.stat.debt b { color: var(--down); }

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

label { display: block; font-weight: 600; font-size: .9rem; margin: 20px 0 4px; }
.field-note { color: var(--text-dim); font-size: .82rem; margin: 0 0 8px; max-width: 66ch; }
.field-note code { background: var(--surface-2); padding: 0 4px; border-radius: 3px; font-size: .92em; }

input[type=text], input[type=search], textarea {
  width: 100%; padding: 9px 12px;
  border: 1px solid var(--line-strong); border-radius: 8px;
  background: var(--surface); color: var(--text); font: inherit; font-size: .95rem;
}
textarea { resize: vertical; line-height: 1.6; }
.mono-input { font-family: var(--mono); font-size: .88rem; }
input:focus, textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; }

.button {
  display: inline-block; padding: 9px 18px; border: 1px solid var(--accent);
  border-radius: 8px; background: var(--accent); color: #fff;
  font: inherit; font-size: .9rem; font-weight: 600; cursor: pointer;
}
.button:hover { text-decoration: none; filter: brightness(1.06); }
.button.ghost { background: none; color: var(--text-dim); border-color: var(--line-strong); }
.button.ghost:hover { color: var(--text); border-color: var(--text-dim); filter: none; }
.button.small { padding: 4px 11px; font-size: .8rem; }
.button.danger { background: var(--danger); border-color: var(--danger); }

.write-form { max-width: 760px; }
.write-actions { display: flex; gap: 10px; margin: 24px 0 8px; align-items: center; }
.write-preview { border-top: 1px solid var(--line); padding-top: 24px; max-width: 760px; }

.flag-form { max-width: 560px; }
.flag-form fieldset { border: 1px solid var(--line); border-radius: var(--radius); padding: 14px 16px; margin: 0; }
.flag-form legend { font-weight: 600; font-size: .9rem; padding: 0 6px; }
.radio { display: flex; align-items: center; gap: 8px; font-weight: 400; margin: 6px 0; }
.radio input { margin: 0; }

.search-page-form { display: flex; gap: 8px; max-width: 520px; margin-bottom: 28px; }
.search-page-form button { padding: 9px 18px; border: 1px solid var(--accent); border-radius: 8px; background: var(--accent); color: #fff; font: inherit; font-weight: 600; cursor: pointer; }

.danger-form { margin-top: 12px; }

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

footer {
  border-top: 1px solid var(--line);
  padding: 28px 20px 48px;
  color: var(--text-dim); font-size: .86rem;
}
footer p { max-width: var(--wrap); margin: 0 auto 8px; }
.footer-note { color: var(--text-faint); font-size: .8rem; max-width: 68ch; }
.footer-links { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.footer-sep { color: var(--line-strong); }

/* --- /admin/research -------------------------------------------------
   The review page for agent research records. Read-only, moderator-only,
   and nothing here is ever rendered to a visitor — see
   services/research.py. */

.rec-finding {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 12px 14px;
  margin: 0 0 10px;
  background: var(--surface);
}
/* An abstention is the schema working, not a gap. It recedes; it does not
   alarm. Colouring it like an error would teach the operator to read honesty
   as failure, which is the one thing that would break this pipeline. */
.rec-finding.is-abstained { background: none; border-style: dashed; }

.rec-head { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; }
.rec-label { font-weight: 600; font-size: .9rem; }
.rec-value { font-size: .9rem; }
.rec-value.stated { color: var(--text); }
.rec-value.negative { color: var(--down); }
.rec-value.abstained { color: var(--text-faint); font-style: italic; }

.rec-quote {
  margin: 10px 0 0;
  padding: 8px 0 8px 12px;
  border-left: 3px solid var(--line-strong);
}
.rec-quote p { margin: 0; font-size: .88rem; color: var(--text-dim); }
.rec-quote cite {
  display: block;
  margin-top: 4px;
  font-family: var(--mono);
  font-size: .72rem;
  font-style: normal;
  word-break: break-all;
}

.rec-flag {
  display: inline-block;
  font-size: .7rem;
  font-family: var(--mono);
  padding: 1px 6px;
  border-radius: 4px;
  background: var(--danger);
  color: #fff;
}
.rec-tag {
  display: inline-block;
  font-size: .7rem;
  font-family: var(--mono);
  padding: 1px 6px;
  border-radius: 4px;
  border: 1px solid var(--line-strong);
  color: var(--text-dim);
}

.rec-alert {
  border: 1px solid var(--danger);
  border-radius: var(--radius);
  padding: 12px 14px;
  margin: 0 0 16px;
}
.rec-alert b { display: block; margin-bottom: 6px; }
.rec-alert ul { margin: 6px 0 0; padding-left: 18px; }
.rec-alert li { font-size: .8rem; }

.rec-summary {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 12px 14px;
  font-size: .92rem;
  line-height: 1.55;
}

/* --- the research block on a coin page -------------------------------
   What a researcher found, as opposed to what a provider said. Present on a
   few hundred coins and absent on the rest — see partials/coin_research.html. */

.research { margin: 28px 0 0; }
.research-sub { color: var(--text-dim); font-size: .85rem; margin: 0 0 14px; }
.research-summary {
  font-size: .98rem;
  line-height: 1.6;
  margin: 0 0 16px;
  padding-left: 14px;
  border-left: 3px solid var(--line-strong);
}

.research-facts {
  list-style: none;
  padding: 0;
  margin: 0 0 14px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 6px;
}
.research-facts li {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 7px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-size: .85rem;
}
.research-label { color: var(--text-dim); }
.research-value { font-family: var(--mono); }
.research-facts li.yes .research-value { color: var(--up); }
.research-facts li.no  .research-value { color: var(--down); }

.research-note { font-size: .87rem; color: var(--text-dim); margin: 0 0 10px; }
.research-claims { font-size: .87rem; color: var(--text-dim); padding-left: 18px; }
.research-claims li { margin-bottom: 6px; }
.research-src { font-size: .75rem; font-family: var(--mono); }
.research-alloc { margin-bottom: 6px; }

.research-sources { font-size: .82rem; margin-top: 14px; }
.research-sources summary { cursor: pointer; color: var(--text-dim); }
.research-sources ul { padding-left: 18px; margin: 8px 0 0; }
.research-sources li { margin-bottom: 3px; word-break: break-all; }

/* --- the daily market vote ------------------------------------------------
 *
 * The second thing on this site granted an exception to the rule this file
 * opens with — that motion and colour are spent on the reactions and on nothing
 * carrying a number — and the exception is drawn narrowly enough to be worth
 * writing down.
 *
 * **The act moves; the figures do not.** Pressing a button is a person
 * arriving, which is exactly the category the rooms already have, so the press
 * gets the flash and the glow. The bars then grow to their share once, on the
 * reveal, because a bar is a picture of a proportion and growing to it is how a
 * picture arrives. What never animates is a *number*: no counting up from zero,
 * no ticking percentage. A number in motion is a number that is wrong for the
 * duration of the animation, and this site does not print a wrong figure in
 * order to look alive. If a rule here ever has to bend for something carrying a
 * number, the answer is still no.
 *
 * Every animation is gated on `.is-cast`, which the server sets on exactly one
 * render — the one immediately after a vote. A panel that replays its reveal on
 * every visit to the front page is a panel people learn to scroll past.
 */

.sentiment {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); padding: 20px 22px; margin-bottom: 40px;
}
.sentiment-ask h2 { font-size: 1.25rem; margin: 0; }
.sentiment-sub { color: var(--text-dim); font-size: .86rem; margin: 4px 0 0; }
/* The clock is a fact and is allowed to move: a clock that does not is broken.
   Tabular figures so "7h 42m" does not shuffle sideways every minute. */
.sentiment-clock { font-size: .86rem; color: var(--text); }
.sentiment-dev { margin-top: 12px; font-size: .86rem; color: var(--text-dim); }

.vote-buttons { display: grid; gap: 12px; grid-template-columns: 1fr 1fr; margin-top: 18px; }
.vote-btn {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  width: 100%; padding: 18px 12px;
  border: 1px solid var(--line-strong); border-radius: var(--radius);
  background: var(--surface-2); color: var(--text);
  font: inherit; cursor: pointer; text-align: center;
  /* The one transition on the resting state. Fast, and on the two properties
     that cost nothing to animate — a `box-shadow` transition on a large element
     is the classic dropped frame on a phone. */
  transition: transform .12s ease, border-color .12s ease, background .12s ease;
}
.vote-btn:hover { text-decoration: none; border-color: var(--accent); transform: translateY(-2px); }
.vote-btn:active { transform: translateY(0) scale(.98); }
.vote-icon { width: 30px; height: 30px; }
.vote-bull .vote-icon { color: var(--up); }
.vote-bear .vote-icon { color: var(--down); }
.vote-bull:hover { border-color: var(--up); background: color-mix(in srgb, var(--up) 9%, var(--surface-2)); }
.vote-bear:hover { border-color: var(--down); background: color-mix(in srgb, var(--down) 9%, var(--surface-2)); }
.vote-label { font-weight: 700; font-size: 1.05rem; letter-spacing: -.01em; }
.vote-blurb { font-size: .8rem; color: var(--text-dim); }
/* Signed out. The same shape, so the panel does not resize when somebody signs
   in and the invitation is the thing they will click next. */
.vote-locked { opacity: .82; }

/* The result. `--share` is set inline per row and is what the bar grows to;
   written as a custom property rather than as a width, because a width set
   inline has nothing to animate *from* on an element that has just been
   inserted by a swap. */
.result { margin-top: 18px; }
.result-you { font-size: .95rem; margin: 0 0 14px; color: var(--text-dim); }
.result-you strong.result-bull { color: var(--up); }
.result-you strong.result-bear { color: var(--down); }

.result-row { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
.result-name { display: inline-flex; align-items: center; gap: 7px; width: 88px; flex: 0 0 auto; font-size: .9rem; font-weight: 600; }
.result-icon { width: 19px; height: 19px; }
.result-bull .result-icon { color: var(--up); }
.result-bear .result-icon { color: var(--down); }
.result-bar { flex: 1 1 auto; height: 12px; border-radius: 999px; background: var(--surface-2); overflow: hidden; }
.result-bar i { display: block; height: 100%; width: var(--share); border-radius: 999px; background: var(--text-faint); }
.result-bull .result-bar i { background: var(--up); }
.result-bear .result-bar i { background: var(--down); }
/* The row the visitor voted for, marked without a second colour: the bars are
   already spending the up/down pair, and a third hue here would make this panel
   the loudest thing on a page whose subject is coins. */
.result-row.is-yours .result-name { color: var(--text); }
.result-row:not(.is-yours) .result-name { color: var(--text-dim); }
.result-figure { display: inline-flex; align-items: baseline; gap: 8px; justify-content: flex-end; width: 92px; flex: 0 0 auto; font-size: .9rem; }
/* The raw count beside the share, always. A rounded 0% can sit on a real vote
   — one bull against 999 bears — and "0%" alone reads as nobody. See
   sentiment.shares. */
.result-count { font-size: .76rem; color: var(--text-faint); }

.sentiment-yesterday {
  margin: 16px 0 0; padding-top: 14px; border-top: 1px solid var(--line);
  font-size: .86rem; color: var(--text-dim);
}
.sentiment-yesterday strong { color: var(--text); }

/* The reveal, once. Bars from nothing to their share, rows staggered so the eye
   follows them down, and one flash across the panel — the same --flash and
   --glow the rooms use, which is deliberate: this is the same kind of event,
   somebody arriving. */
.sentiment.is-cast { animation: sentiment-land .6s ease-out; }
.sentiment.is-cast .result-bar i { animation: sentiment-fill .75s cubic-bezier(.22,.9,.3,1) both; }
.sentiment.is-cast .result-row:nth-of-type(1) .result-bar i { animation-delay: .05s; }
.sentiment.is-cast .result-row:nth-of-type(2) .result-bar i { animation-delay: .16s; }
/* The figures fade in where the bar lands rather than counting up to it. They
   are *not* in motion — the value on screen is the final one from the first
   frame, and only its opacity changes. */
.sentiment.is-cast .result-figure,
.sentiment.is-cast .result-you { animation: sentiment-say .5s ease-out both; animation-delay: .3s; }
/* And the row you chose gets the glow, on the row rather than on a number. */
.sentiment.is-cast .result-row.is-yours { animation: sentiment-yours .9s ease-out; border-radius: 8px; }

@keyframes sentiment-land {
  0%   { box-shadow: 0 0 0 0 var(--glow); }
  35%  { box-shadow: 0 0 0 6px var(--flash); }
  100% { box-shadow: 0 0 0 0 transparent; }
}
@keyframes sentiment-fill { from { width: 0; } to { width: var(--share); } }
@keyframes sentiment-say { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; } }
@keyframes sentiment-yours {
  0%   { background: var(--flash); }
  100% { background: transparent; }
}

@media (max-width: 520px) {
  .sentiment { padding: 16px; }
  .result-name { width: 66px; font-size: .84rem; }
  .result-figure { width: 78px; }
}

/* --- classification ------------------------------------------------------- */

.tag-group { margin-bottom: 32px; }
.tag-cloud { list-style: none; margin: 12px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
.tag-link {
  display: inline-flex; align-items: baseline; gap: 8px;
  padding: 7px 13px; border: 1px solid var(--line); border-radius: 999px;
  background: var(--surface); color: var(--text); font-size: .9rem;
}
.tag-link:hover { border-color: var(--accent); text-decoration: none; }
.tag-count { font-size: .78rem; color: var(--text-faint); }

/* --- the dashboard --------------------------------------------------------
 *
 * A grid of saved coins the owner arranges by hand. The drag itself is in
 * dashboard.html; what is here is the two states it needs — the card in hand,
 * and the grid while one is.
 */

.dash-private { margin-bottom: 16px; }
.dash-hint { color: var(--text-dim); font-size: .86rem; margin: 0 0 14px; }
.dash-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }

.dash-card {
  position: relative; display: flex; align-items: stretch;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  /* `grab` on the whole card, because the whole card is what moves — the grip
     is a sign, not a handle. `touch-action: none` is what makes the drag work on
     a phone at all: without it the browser claims the gesture for scrolling
     before a single pointermove reaches the page. */
  cursor: grab; touch-action: none;
  transition: border-color .12s ease, box-shadow .12s ease, transform .12s ease;
}
.dash-card:hover { border-color: var(--line-strong); }
.dash-grip { display: flex; align-items: center; padding: 0 2px 0 8px; color: var(--text-faint); flex: 0 0 auto; }
.grip-icon { width: 16px; height: 16px; }
.dash-body { display: block; flex: 1 1 auto; padding: 14px 14px 14px 6px; color: var(--text); min-width: 0; }
.dash-body:hover { text-decoration: none; }
.dash-head { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.dash-price { display: block; font-size: 1.2rem; margin-top: 10px; letter-spacing: -.01em; }
.dash-moves { display: flex; flex-wrap: wrap; gap: 4px 16px; margin-top: 8px; }
.dash-move { display: flex; align-items: baseline; gap: 6px; font-size: .86rem; }
.dash-move-key { font-size: .72rem; color: var(--text-faint); text-transform: uppercase; letter-spacing: .04em; }
/* The day and the week take the up/down pair; the market cap does not — it is a
   size, not a direction, and colouring it green would say something the number
   does not. `.dash-move` carries the class, so the key and the figure tint
   together. */
.dash-move.up b { color: var(--up); }
.dash-move.down b { color: var(--down); }
.dash-age { display: block; font-size: .74rem; color: var(--text-faint); margin-top: 8px; }

/* Un-save, from the card. Faint until the card is hovered: it is the one
   destructive control on a page of links, and a Remove button at full contrast
   on twenty cards is what the page would look like it was *for*. */
.dash-remove { position: absolute; top: 8px; right: 8px; margin: 0; }
.dash-remove button {
  border: 1px solid transparent; background: none; color: var(--text-faint);
  font: inherit; font-size: .74rem; padding: 2px 8px; border-radius: 6px;
  cursor: pointer; opacity: 0; transition: opacity .12s ease;
}
.dash-card:hover .dash-remove button { opacity: 1; }
/* Never hidden from the keyboard. `opacity` rather than `display` is what makes
   that possible — a focused button that cannot be seen is worse than one always
   visible, so focus reveals it exactly as hover does. */
.dash-remove button:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 1px; }
.dash-remove button:hover { color: var(--danger); border-color: var(--danger); }

.dash-card.is-dragging {
  cursor: grabbing; z-index: 2;
  border-color: var(--accent); box-shadow: 0 8px 24px rgba(0, 0, 0, .18);
  transform: scale(1.02) rotate(-.4deg);
}
/* Everything not in hand recedes, so the card being moved is unmistakably the
   one under the pointer. */
.dash-grid.is-reordering .dash-card:not(.is-dragging) { opacity: .55; }
/* The drop. One beat of the accent ring — the same vocabulary as the vote and
   as an arriving reaction, and for the same reason: something a person just
   did. */
.dash-card.is-dropped { animation: dash-drop .4s ease-out; }
@keyframes dash-drop {
  0%   { box-shadow: 0 0 0 0 var(--glow); }
  40%  { box-shadow: 0 0 0 5px var(--flash); }
  100% { box-shadow: 0 0 0 0 transparent; }
}

.account-block { margin-bottom: 32px; }
.signal-saved-note { font-size: .82rem; color: var(--text-dim); margin: -8px 0 20px; }

/* --- motion, refused ------------------------------------------------------
 *
 * Everything above that moves, stopped. Not softened — stopped: the reveal, the
 * drop, the arrival flashes in the rooms, the card lift. Each one is decoration
 * over a state the page already states in words and figures, so there is
 * nothing to lose by removing it, and for a reader who has asked for no motion a
 * "reduced" animation is still an animation.
 *
 * The one thing that keeps working is the countdown, which is not an animation
 * at all — it is a clock, drawn by a script, and a clock that stops is broken.
 */
@media (prefers-reduced-motion: reduce) {
  .sentiment.is-cast,
  .sentiment.is-cast .result-bar i,
  .sentiment.is-cast .result-figure,
  .sentiment.is-cast .result-you,
  .sentiment.is-cast .result-row.is-yours,
  .dash-card.is-dropped,
  .chat-item.is-new { animation: none !important; }
  .vote-btn, .dash-card { transition: none; }
  .vote-btn:hover, .vote-btn:active, .dash-card.is-dragging { transform: none; }
  /* The bar still has to end up at its share. The animation is what sets the
     width during the reveal, so without this the bars would sit at zero for a
     reader who asked for no motion. */
  .result-bar i { width: var(--share); }
}
