/* FlowPulse design system — tokens and component primitives.
 *
 * Layer 00. Loads BEFORE 01-base.css and every layer after it, so anything
 * here can be overridden by a later layer exactly as before. Nothing in this
 * file targets an existing selector: it defines custom properties on :root and
 * a set of `fp-` prefixed classes. That is deliberate — adding a layer to a
 * 16-layer cascade whose document order is load-bearing must not be able to
 * change what any existing rule computes to.
 *
 * WHY THIS EXISTS (UI plan, Phase 0): one source of truth for every card,
 * button, heading and gap. Values below are not new inventions — they are the
 * values already in use across layers 01–16, named. The palette was taken from
 * a frequency count of every hex literal in public/css, so adopting a token is
 * a refactor with an identical computed value, provable with `npm run
 * css:snapshot`.
 *
 * THE ONE RULE THAT SHAPES THE PALETTE: if the accent is everywhere, it is not
 * an accent. Neutrals and whitespace dominate; --fp-brand marks the single most
 * important thing on a screen and nothing else.
 */

:root {
  /* ---------------------------------------------------------------------
   * SURFACES — darkest at the back, lightest at the front.
   * ------------------------------------------------------------------ */
  --fp-bg:            #071017;  /* page background */
  --fp-bg-deep:       #050b10;  /* behind the page: overlays, wells, modals */
  --fp-surface:       #0b1218;  /* the standard card */
  --fp-surface-raised:#121a21;  /* a card sitting on a card */
  --fp-surface-sunk:  #0a1218;  /* inputs and inset panels */

  /* Hairlines. Opacity rather than a hex so they sit on any surface.
     These three are DIVIDERS — the edge of a card, a rule between rows. They
     are meant to be quiet and they are correct at these values. */
  --fp-border:        rgba(255,255,255,.08);
  --fp-border-soft:   rgba(255,255,255,.06);
  --fp-border-strong: rgba(255,255,255,.12);

  /* The edge of an INTERACTIVE CONTROL, which is a different job from a
     divider and needs a different value.
     --fp-border-strong drew every non-primary button, every <select> and every
     <input>, and it measures **1.3:1** against a card — so a control's only
     boundary was invisible, and a select's fill (--fp-surface-sunk #0a1218) is
     within a shade of the card behind it. The result: dropdowns that could not
     be found, and a row of unselected buttons that read as greyed-out rather
     than clickable. Both reported from a phone, one as "hard to see" and one as
     "it locks as grey then green".
     WCAG 1.4.11 asks 3:1 for the visual boundary of a UI component; this
     measures **3.3:1** against the darkest surface in the product (the page
     background showing through a card whose fill is a gradient, not a colour)
     and 3.5:1 on the standard card. */
  --fp-border-control: rgba(255,255,255,.38);

  /* ---------------------------------------------------------------------
   * TEXT — four steps, and only four. More than four and nothing reads as
   * emphasis. Every step is checked against --fp-surface for contrast.
   *
   * "Checked" now means measured, not eyeballed. Ratios against the three
   * backgrounds text actually sits on — --fp-surface #0b1218, --fp-bg #071017
   * and the #0d1a24 card — are:
   *
   *   --fp-text        16.1 / 16.4 / 15.1
   *   --fp-text-body   12.4 / 12.6 / 11.6
   *   --fp-text-muted   6.6 /  6.7 /  6.2
   *   --fp-text-faint   4.9 /  5.3 /  4.9
   *
   * WCAG AA wants 4.5:1 for text below 24px. The faint step was #5a6770 and
   * measured 3.2 / 3.3 / 3.0 — it failed on every surface in the product, and
   * it is not decoration: it carries coin tickers, timestamps and the "/100"
   * on the health gauge. It is now #7a8993, the lightest-touch value that
   * clears 4.5 on all three. It stays a clear step below --fp-text-muted, so
   * the four-step hierarchy and the calm dark look are unchanged — the bottom
   * step is simply legible now.
   * ------------------------------------------------------------------ */
  --fp-text:          #ffffff;  /* headings, metrics, the number you came for */
  --fp-text-body:     #d7e2e9;  /* prose */
  --fp-text-muted:    #8e9aa5;  /* labels, captions, secondary lines */
  --fp-text-faint:    #7a8993;  /* timestamps, disabled, "nothing here yet" */

  /* ---------------------------------------------------------------------
   * BRAND AND SEMANTIC — the product is green and black. #00ff99.
   *
   * A teal brand (#00d5c8) was tried on 6 August and REVERTED the same day on
   * Ryan's call: "we want to change back to our green, not the blue." It read
   * as blue on his screen, and for a brand colour that is the only test that
   * counts. The 1.2 vision document's "teal is the only accent colour" line is
   * superseded by this — do not reinstate it from that document.
   *
   * WHAT THE EXPERIMENT WAS FOR, since the argument will come back: while brand
   * and gain are the same green, every accent on the page reads as a market
   * signal. A green "View track record" button is not telling you anything went
   * up, but it wears the colour that does. That tension is real and is now
   * KNOWINGLY accepted — green and black is the product's identity, and
   * identity beats tidiness.
   *
   * IF IT EVER NEEDS SOLVING, the cheap version is to move --fp-gain to a
   * slightly different green rather than move the brand: one line, just below,
   * and the brand stays the colour everyone already recognises.
   *
   * That sentence was ASPIRATIONAL until 6 Aug and is now true. The accent pass
   * moved 241 brand literals onto var(--fp-brand) and did not touch the
   * direction pair, so 39 sites still hardcoded #00ff99 / #ff4d4d — moving the
   * token would have split the palette, greens in CSS following it and every
   * JS-rendered green staying behind. All 39 are on the tokens now, including
   * the Chart.js series, which READS this value at creation time because a
   * canvas cannot resolve a custom property. Do not reintroduce a literal:
   * the moment one exists, the line below stops being one line.
   *
   * What the experiment left behind and is worth keeping: every accent in the
   * product now resolves through these tokens instead of 241 hardcoded
   * literals. That is the only reason this revert was a four-line edit.
   * ------------------------------------------------------------------ */
  --fp-brand:         #00ff99;
  --fp-brand-ink:     #04231a;  /* text on a solid brand fill */
  --fp-brand-08:      rgba(0,255,153,.08);
  --fp-brand-12:      rgba(0,255,153,.12);
  --fp-brand-30:      rgba(0,255,153,.30);
  --fp-brand-40:      rgba(0,255,153,.40);

  /* The channel triple, for the alphas the four steps above do not cover.
   * Layers 01–16 use the accent at roughly twenty different opacities — .04 for
   * a row tint, .8 for a glow — and naming a token for each would be a worse
   * lie than the hex it replaced. `rgba(var(--fp-brand-rgb), .35)` keeps every
   * one of them pointing at this line, which is the whole point: the palette
   * moves from one place or it is not a design system. */
  --fp-brand-rgb:     0,255,153;
  --fp-gain-rgb:      0,255,153;
  /* Added 6 Aug with the major-move colour fix: the whale/"big move" surfaces
     carried a hardcoded #0096ff blue and a rgba(0,150,255,…) glow. Direction
     colour replaced both, and the glow needed a channel triple to do it. */
  --fp-loss-rgb:      255,77,77;

  /* Same green as the brand, deliberately. Kept as its own NAME so intent still
     reads at the call site — "this number went up" versus "this is our accent" —
     and so the two can be pulled apart later from one line if they ever need to
     be. See the brand note above for why they are not pulled apart today. */
  --fp-gain:          #00ff99;
  --fp-loss:          #ff4d4d;
  --fp-warn:          #ffcc33;
  --fp-neutral:       #8e9aa5;

  /* The middle of the SIGNAL STRENGTH ramp — added 7 Aug when strength stopped
     borrowing --fp-warn. Amber meant "middling score" and "be careful" at the
     same time, an inch apart in the same table row.
     A dimmed brand green: 7.8:1 on --fp-surface, and clearly below the full
     brand at 14.2:1. Used where magnitude is already carried by something else
     (a bar's width, a number) — the pills ramp by FILL instead, because a
     green-vs-grey ramp is unreadable to the people most likely to need it. */
  --fp-strength-mid:  #4fb98a;

  /* ---------------------------------------------------------------------
   * TYPE — one typeface, five roles. Line-heights are set with the size so a
   * role cannot be half-adopted.
   * ------------------------------------------------------------------ */
  --fp-font: Arial, Helvetica, sans-serif;

  --fp-size-display: 40px; --fp-lh-display: 1.1;   /* hero only, one per page */
  --fp-size-h1:      28px; --fp-lh-h1:      1.2;
  --fp-size-h2:      20px; --fp-lh-h2:      1.3;
  --fp-size-h3:      16px; --fp-lh-h3:      1.4;
  --fp-size-body:    14px; --fp-lh-body:    1.6;   /* 1.6 — prose needs air */
  --fp-size-caption: 12px; --fp-lh-caption: 1.5;

  /* Metrics are their own role: tabular figures so a column of prices does not
     shuffle sideways as digits change every four seconds. */
  --fp-size-metric:  32px; --fp-lh-metric:  1.1;
  --fp-num: "kern" 1, "tnum" 1;

  --fp-track-tight: -.02em;  /* headings */
  --fp-track-label: .04em;   /* uppercase labels */

  /* ---------------------------------------------------------------------
   * SPACING — strict 8px grid. --fp-1 is the only half-step, for optical
   * nudges inside a component; layout gaps use --fp-2 and up.
   * ------------------------------------------------------------------ */
  --fp-1: 4px;
  --fp-2: 8px;
  --fp-3: 16px;
  --fp-4: 24px;
  --fp-5: 32px;
  --fp-6: 40px;
  --fp-7: 48px;
  --fp-8: 64px;

  /* ---------------------------------------------------------------------
   * SHAPE, DEPTH, MOTION — one of each.
   * ------------------------------------------------------------------ */
  --fp-radius-sm:   8px;
  --fp-radius:     12px;   /* the default: buttons, inputs, rows */
  --fp-radius-lg:  18px;   /* cards and panels */
  --fp-radius-pill: 999px;

  --fp-shadow:      0 0 30px rgba(0,0,0,.4);
  --fp-shadow-lift: 0 8px 24px rgba(0,0,0,.5);

  /* One animation speed, everywhere. */
  --fp-motion: 200ms;
  --fp-ease: ease;
  --fp-transition:
    background var(--fp-motion) var(--fp-ease),
    color var(--fp-motion) var(--fp-ease),
    border-color var(--fp-motion) var(--fp-ease),
    box-shadow var(--fp-motion) var(--fp-ease),
    transform var(--fp-motion) var(--fp-ease);

  /* Focus ring. Defined once so it can never drift between controls. */
  --fp-focus: 2px solid var(--fp-brand);
  --fp-focus-offset: 2px;
}

/* Motion is decoration. Anyone who has asked the OS to stop it gets no
   transition, no transform and no looping animation anywhere in the system. */
@media (prefers-reduced-motion: reduce) {
  :root { --fp-motion: 1ms; }
  /* Every looping animation in the product, off. Layer 00 loads first, so this
     is stated as a blanket rule rather than chased through sixteen layers —
     someone who has asked their OS to stop motion has asked once, and should
     not have to hope each individual component remembered. */
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
}

/* =======================================================================
 * COMPONENT PRIMITIVES
 *
 * Additive only — `fp-` prefixed classes that exist nowhere else in the
 * codebase. Existing markup is untouched until a page is deliberately moved
 * onto them, which is what keeps Phase 0 a zero-risk change.
 * ==================================================================== */

/* ── Card: one card, used everywhere ─────────────────────────────────── */
.fp-card {
  background: var(--fp-surface);
  border: 1px solid var(--fp-border);
  border-radius: var(--fp-radius-lg);
  padding: var(--fp-4);
  box-shadow: var(--fp-shadow);
}
.fp-card--flush { padding: 0; overflow: hidden; }
.fp-card--quiet { background: transparent; box-shadow: none; }

.fp-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--fp-3);
  margin-bottom: var(--fp-3);
  flex-wrap: wrap;
}
/* On a phone the title and its action fight for one line and both wrap
   mid-phrase — "Track Record / (All- / time)" beside "View full track / record".
   Stack them instead: the heading gets the full width, the action sits under it
   and stays a single line. */
@media (max-width: 560px) {
  .fp-card-head { flex-direction: column; align-items: flex-start; gap: var(--fp-2); }
  .fp-card-head > * { max-width: 100%; }
}
/* Flex, because Lucide swaps the <i data-lucide> for an <svg display:block>
   and drops the inline styles that were on it — an icon inside a heading
   otherwise sits on its own line above the words. Sizing lives here for the
   same reason: it has to survive the swap. */
.fp-card-title {
  margin: 0;
  display: flex;
  align-items: center;
  gap: var(--fp-2);
  font-size: var(--fp-size-h3);
  line-height: var(--fp-lh-h3);
  letter-spacing: var(--fp-track-tight);
  color: var(--fp-text);
  font-weight: 700;
}
/* Card-title icons are MUTED, not brand green.
   Eighteen accent-coloured icons were on Home at once — one on every card
   heading — which is the rule about accents breaking itself: if it marks every
   card, it marks none of them. Green is left for the things a person can act
   on and for the one number the screen is about. */
.fp-card-title svg { width: 16px; height: 16px; flex-shrink: 0; color: var(--fp-text-muted); }
.fp-icon-btn svg { width: 17px; height: 17px; }
.fp-linkbtn svg { width: 13px; height: 13px; }
.fp-btn svg { width: 14px; height: 14px; flex-shrink: 0; }
.fp-nav-promo-head svg { width: 14px; height: 14px; color: var(--fp-brand); }

/* ── Type roles ──────────────────────────────────────────────────────── */
.fp-display { font-size: var(--fp-size-display); line-height: var(--fp-lh-display); letter-spacing: var(--fp-track-tight); margin: 0; }
.fp-h1      { font-size: var(--fp-size-h1);      line-height: var(--fp-lh-h1);      letter-spacing: var(--fp-track-tight); margin: 0; }
.fp-h2      { font-size: var(--fp-size-h2);      line-height: var(--fp-lh-h2);      letter-spacing: var(--fp-track-tight); margin: 0; }
.fp-body    { font-size: var(--fp-size-body);    line-height: var(--fp-lh-body);    color: var(--fp-text-body); }
.fp-caption { font-size: var(--fp-size-caption); line-height: var(--fp-lh-caption); color: var(--fp-text-muted); }
.fp-label {
  font-size: var(--fp-size-caption);
  letter-spacing: var(--fp-track-label);
  text-transform: uppercase;
  color: var(--fp-text-muted);
  font-weight: 700;
}
.fp-metric {
  font-size: var(--fp-size-metric);
  line-height: var(--fp-lh-metric);
  font-weight: 700;
  color: var(--fp-text);
  font-feature-settings: var(--fp-num);
}
/* Any live number, at any size. Tabular figures stop a 4-second refresh from
   making the whole row twitch as digit widths change. */
.fp-num { font-feature-settings: var(--fp-num); }

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

/* ── Button: one button, three states ────────────────────────────────── */
.fp-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--fp-2);
  padding: 10px var(--fp-3);
  border-radius: var(--fp-radius);
  /* Control boundary, not a divider — see the token. At --fp-border-strong an
     unselected button was indistinguishable from the card it sat on, which is
     why a row of them read as disabled until one went green. */
  border: 1px solid var(--fp-border-control);
  background: rgba(255,255,255,.04);
  color: var(--fp-text-body);
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;      /* the component is used on <a> as well as <button> */
  transition: var(--fp-transition);
}
.fp-btn:hover { border-color: var(--fp-brand-30); color: var(--fp-text); }
.fp-btn:focus-visible { outline: var(--fp-focus); outline-offset: var(--fp-focus-offset); }

.fp-btn--primary {
  background: var(--fp-brand);
  border-color: transparent;
  color: var(--fp-brand-ink);
}
.fp-btn--primary:hover { filter: brightness(1.06); color: var(--fp-brand-ink); }

.fp-btn--ghost { background: transparent; border-color: var(--fp-brand-40); color: var(--fp-brand); }

.fp-btn:disabled,
.fp-btn[aria-disabled="true"] {
  opacity: .5;
  cursor: not-allowed;
  transform: none;
  border-color: var(--fp-border);
  color: var(--fp-text-faint);
}
.fp-btn:disabled:hover { filter: none; border-color: var(--fp-border); }

/* ── Skeleton: what a card shows while it is loading ─────────────────────
 * Replaces "Loading…" and "--". A shape that matches what is coming reads as
 * progress; a dash reads as a number we failed to get. */
.fp-skeleton {
  display: block;
  border-radius: var(--fp-radius-sm);
  background: linear-gradient(
    90deg,
    rgba(255,255,255,.04) 25%,
    rgba(255,255,255,.09) 37%,
    rgba(255,255,255,.04) 63%
  );
  background-size: 400% 100%;
  animation: fpShimmer 1.4s ease infinite;
}
@keyframes fpShimmer {
  0%   { background-position: 100% 50%; }
  100% { background-position: 0 50%; }
}
.fp-skeleton--text   { height: 12px; margin: var(--fp-2) 0; }
.fp-skeleton--title  { height: 18px; width: 45%; margin: var(--fp-2) 0; }
.fp-skeleton--metric { height: 30px; width: 35%; margin: var(--fp-2) 0; }
.fp-skeleton--row    { height: 44px; margin-bottom: var(--fp-2); }
.fp-skeleton--w70 { width: 70%; }
.fp-skeleton--w50 { width: 50%; }
.fp-skeleton--w30 { width: 30%; }

/* ── Empty state: what a card shows when there is genuinely nothing ───── */
.fp-empty {
  text-align: center;
  padding: var(--fp-5) var(--fp-4);
  color: var(--fp-text-muted);
}
.fp-empty svg { margin: 0 auto var(--fp-2); color: var(--fp-text-faint); }
.fp-empty-title {
  color: var(--fp-text-body);
  font-size: var(--fp-size-body);
  font-weight: 700;
  margin-bottom: var(--fp-1);
}
.fp-empty-body { font-size: 13px; line-height: var(--fp-lh-body); max-width: 42ch; margin: 0 auto; }
.fp-empty .fp-btn { margin-top: var(--fp-3); }

/* ── Wordmark ────────────────────────────────────────────────────────────
 * Was a 24–30px raster logo.jpg, which is soft on every retina screen and
 * carries its own background so it can never sit cleanly on a dark surface.
 * Inline SVG instead: crisp at any size, one fewer request, and the glyph
 * takes --fp-brand while the text takes the surrounding colour, so the mark
 * works on the sidebar, the mobile sheet and a light surface unchanged. */
.fp-wordmark {
  display: inline-flex;
  align-items: center;
  gap: var(--fp-2);
  font-weight: 700;
  letter-spacing: var(--fp-track-tight);
  color: var(--fp-brand);
  white-space: nowrap;
}
.fp-wordmark svg {
  width: 1.05em;
  height: 1.05em;
  flex-shrink: 0;
  color: var(--fp-brand);
  overflow: visible;
}

/* ── Home header ─────────────────────────────────────────────────────────
 * The design target opens by addressing the person. Wraps on narrow screens
 * rather than squeezing the greeting, because a truncated name is worse than
 * a stacked button. */
.fp-home-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--fp-3);
  flex-wrap: wrap;
  margin-bottom: var(--fp-3);
}

/* ── Market Health gauge ─────────────────────────────────────────────────
 * One read replacing five stat cards. The ring is drawn with stroke-dasharray
 * on a plain SVG circle — no charting library for a single arc, and it stays
 * crisp at any size. The arc is rotated so 0 starts at the top. */
.fp-health-body {
  display: flex;
  align-items: center;
  gap: var(--fp-4);
  flex-wrap: wrap;
}
.fp-health-read { flex: 1 1 260px; min-width: 0; }

.fp-gauge {
  position: relative;
  width: 132px;
  height: 132px;
  flex-shrink: 0;
  text-align: center;
}
.fp-gauge svg {
  width: 132px;
  height: 132px;
  transform: rotate(-90deg);   /* 0 at 12 o'clock */
}
.fp-gauge-track,
.fp-gauge-fill {
  fill: none;
  stroke-width: 9;
  stroke-linecap: round;
}
.fp-gauge-track { stroke: rgba(255,255,255,.07); }
.fp-gauge-fill {
  stroke: var(--fp-brand);
  /* 2πr for r=52. Set stroke-dashoffset to (1 - value/100) × this. */
  stroke-dasharray: 326.7;
  stroke-dashoffset: 326.7;    /* empty until real data arrives */
  transition: stroke-dashoffset var(--fp-motion) var(--fp-ease),
              stroke var(--fp-motion) var(--fp-ease);
}
/* Everything sits INSIDE the ring, stacked. An absolutely-positioned label
   under the gauge collided with the arc at the 6 o'clock position. */
.fp-gauge-centre {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  padding: 0 22px;             /* keep long labels clear of the stroke */
  pointer-events: none;
}
.fp-gauge-value {
  font-size: 28px;
  font-weight: 800;
  line-height: 1;
  color: var(--fp-text);
}
.fp-gauge-scale {
  font-size: 11px;
  color: var(--fp-text-faint);
}
.fp-gauge-label {
  margin-top: 3px;
  font-size: 11px;
  font-weight: 700;
  line-height: 1.2;
  color: var(--fp-text-muted);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (max-width: 600px) {
  .fp-health-body { justify-content: center; }
  .fp-health-read { flex-basis: 100%; text-align: center; }
}

/* ═══════════════════════════════════════════════════════════════════════
 * HOME — the design target's Today page.
 * ════════════════════════════════════════════════════════════════════ */

/* Two columns on Home, three when a coin is open.
 *
 * The shell is `230px | content | 380px` — a permanent right rail holding the
 * coin drawer. On Home that rail is 380px of "No coin selected", which is a
 * third of a laptop screen spent on an empty state, and it squeezes the Home
 * cards into a column narrower than the target's. The design target has no
 * right rail on Today at all.
 *
 * `body.fp-no-drawer` collapses it, and the drawer comes back the moment a coin
 * is opened or the Markets view is active. Driven by applyView(), so there is
 * one place that decides what a view looks like. */
body.fp-no-drawer .app-shell { grid-template-columns: 230px minmax(0, 1fr); }
body.fp-no-drawer .right-panel { display: none; }

/* Two columns of unequal weight, matching the target: the left card in each
   row carries the primary read, the right one supports it. Collapses to a
   single column below 1100px — two of these side by side stop being readable
   long before a phone. */
.fp-home-grid {
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  gap: var(--fp-3);
  align-items: start;
  margin-bottom: var(--fp-3);
}
/* 1.2: ONE hero per page, everything else supports it.
 *
 * Market Health and 60 Second Market used to sit side by side at 1.15fr / 1fr —
 * near-equal weight, which is two heroes and therefore none. The vision names
 * Market Health "large hero" and 60 Second Market "the single most important
 * component… this IS FlowPulse's identity", so both take the full width and
 * the eye gets an order to follow: the score, then the sentence explaining it,
 * then the supporting pair. */
.fp-home-grid > #marketStatus,
.fp-home-grid > #sixtySecond { grid-column: 1 / -1; }
.fp-home-grid > .main-card { margin: 0; }

/* The hero read. Full width would otherwise stretch the explanation to a
   140-character line, which is unreadable at any font size — so the gauge
   grows and the prose is capped at a measure instead of filling the card. */
#marketStatus .fp-gauge,
#marketStatus .fp-gauge svg { width: 168px; height: 168px; }
#marketStatus .fp-gauge-value { font-size: 40px; }
#marketStatus .fp-gauge-label { font-size: 12px; }
#marketStatus .fp-health-body { gap: var(--fp-5); }
#marketStatus #marketStatusText {
  font-size: 17px;
  line-height: 1.55;
  color: var(--fp-text);
  max-width: 68ch;
}

/* 60 Second Market is prose and nothing else, so it gets a measure too. The
   vision caps it at three short paragraphs; this caps the line length that
   makes three paragraphs readable. */
#sixtySecond #sixtySecondBody {
  max-width: 78ch;
  font-size: 15px;
  line-height: var(--fp-lh-body);
}

/* Below 700px the gauge and the reading stack, and a 168px ring plus a 17px
   paragraph is most of a phone screen before anything else appears. Back to
   the standard sizes — the hierarchy is already carried by document order once
   there is only one column. */
@media (max-width: 700px) {
  #marketStatus .fp-gauge,
  #marketStatus .fp-gauge svg { width: 132px; height: 132px; }
  #marketStatus .fp-gauge-value { font-size: 28px; }
  #marketStatus #marketStatusText { font-size: 15px; }
  #marketStatus .fp-health-body { gap: var(--fp-3); }
}

@media (max-width: 1100px) {
  .fp-home-grid { grid-template-columns: 1fr; }
}

/* The live pill in its new home. It was a stacked block in the nav rail; here
   it is one inline chip beside the greeting subline. Higher specificity than
   the sidebar rules it inherits, and scoped so it only applies in the header. */
.fp-home-head .live-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--fp-2);
  margin: 0;
  padding: 4px 10px;
  min-height: 0;
  background: rgba(255,255,255,.04);
  border: 1px solid var(--fp-border);
  border-radius: var(--fp-radius-pill);
  font-size: 11px;
  white-space: nowrap;
}
.fp-home-head .live-pill #lastUpdated {
  margin: 0;
  color: var(--fp-text-faint);
  font-size: 11px;
}

/* A tooltip at the right edge of a card head.
 *
 * .tooltip-text is `left:50%; transform:translateX(-50%)` — fine for the Market
 * Overview stats it was built for, which sit mid-card. In a card head the
 * trigger is hard against the right edge, so half of a 200px bubble hung off
 * the page and put 55px of horizontal scroll on the whole document at 900px.
 * Caught by the viewport sweep, not by looking at it on a desktop.
 *
 * Pinned to the right edge here instead, and capped so it can never be wider
 * than the card it sits in. */
.fp-card-head .tooltip-text {
  left: auto;
  right: 0;
  transform: none;
  width: min(260px, calc(100vw - 48px));

  /* Opens DOWNWARD. `.tooltip-text` defaults to `bottom: calc(100% + 8px)`,
     which is right for the market stats it was built for — they sit mid-card
     with room above. A card head is by definition the top of its card, and on
     a phone the Market Health head is near the top of the PAGE, so the bubble
     opened straight off the top of the viewport and the text could not be
     read at all. There is always room below a heading; there is not always
     room above one. */
  top: calc(100% + 8px);
  bottom: auto;
}

/* On a phone the bubble is anchored to the CARD HEAD, not to the little "?".
 *
 * Anchoring to the trigger cannot be made safe at narrow widths: the head
 * stacks below 560px, so the trigger sits wherever the text before it ends.
 * Right-anchoring pushed 108px of the bubble off the left edge; left-anchoring
 * then pushed it 76px off the right. Both measured, neither fixable by picking
 * the other side — the trigger's position is the problem.
 *
 * Making the head the containing block sidesteps it entirely: the bubble spans
 * the card and is on screen by construction, wherever the "?" happens to land. */
@media (max-width: 560px) {
  .fp-card-head { position: relative; }
  .fp-card-head .tooltip-wrap { position: static; }
  .fp-card-head .tooltip-text {
    left: 0;
    right: 0;
    width: auto;
    top: calc(100% + 8px);
  }
}

/* ── Compare Coins ───────────────────────────────────────────────────────
 * The copy says "side by side", so it has to BE side by side. It was
 * `display:flex; flex-wrap:wrap` with `min-width:210px` columns, which is
 * side by side on a desktop and stacked on a phone — and a comparison you
 * have to scroll between is not a comparison, it is two cards.
 *
 * A two-column grid that never wraps. `min-width: 0` on the children is
 * load-bearing: grid children default to `min-width: auto`, so without it the
 * sparkline's intrinsic width would push the tracks wider than the card and
 * put the page into horizontal scroll — the standing rule that causes nearly
 * every overflow bug in this codebase. */
.fp-compare-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--fp-2);
  align-items: start;
}
.fp-compare-grid > * { min-width: 0; }

/* Narrow phones: the label/value rows stop fitting on one line long before the
   columns stop fitting on screen, so they stack inside each column while the
   two columns stay beside each other. */
@media (max-width: 560px) {
  .fp-compare-grid .detail-line {
    display: block;
    text-align: left;
  }
  .fp-compare-grid .detail-line strong { display: block; }
  .fp-compare-grid { font-size: 12px; }
}

/* ── Sign-up banner ──────────────────────────────────────────────────────
 * Signed-out visitors, top of Home, full width. The accent is spent here
 * deliberately: this is the one action we want a new visitor to take, and the
 * rule that an accent marks the single most important thing on a screen is
 * exactly why it belongs on this and not on the chrome around it. */
.fp-personalise {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--fp-4);
  flex-wrap: wrap;
  margin-bottom: var(--fp-3);
  padding: var(--fp-3) var(--fp-4);
  border: 1px solid var(--fp-brand-30);
  border-radius: var(--fp-radius-lg);
  background: linear-gradient(120deg, var(--fp-brand-12), var(--fp-brand-08) 55%, transparent);
}
.fp-personalise-title {
  font-size: var(--fp-size-h3);
  font-weight: 700;
  color: var(--fp-text);
  margin-bottom: 3px;
}
.fp-personalise-body {
  font-size: 13px;
  line-height: var(--fp-lh-body);
  color: var(--fp-text-body);
  max-width: 62ch;
}
.fp-personalise .fp-btn { padding: 11px 22px; font-size: 14px; }

@media (max-width: 620px) {
  .fp-personalise { flex-direction: column; align-items: stretch; text-align: left; }
  .fp-personalise .fp-btn { width: 100%; }
}

/* Header actions */
.fp-home-actions { display: flex; align-items: center; gap: var(--fp-2); }
.fp-icon-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: var(--fp-radius);
  border: 1px solid var(--fp-border-strong);
  background: rgba(255,255,255,.04);
  color: var(--fp-text-body);
  cursor: pointer;
  transition: var(--fp-transition);
}
.fp-icon-btn:hover { border-color: var(--fp-brand-30); color: var(--fp-text); }
.fp-icon-dot {
  position: absolute;
  top: 8px;
  right: 9px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--fp-loss);
}
.fp-avatar {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: 1px solid var(--fp-border-control);
  background: rgba(255,255,255,.06);
  color: var(--fp-text);
  font-family: inherit;
  font-weight: 700;
  font-size: 14px;
  cursor: pointer;
  text-transform: uppercase;
  transition: var(--fp-transition);
}
.fp-avatar:hover { border-color: var(--fp-brand-30); }

/* A text link that is a button. Used for every "→" action in the target.
   Vertical padding is not cosmetic: at font-size 12 the hit area measured 14px
   tall, and WCAG 2.2 AA (2.5.8) wants 24. Padding rather than a height, so the
   text sits where it always did and only the target grows around it. */
.fp-linkbtn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: none;
  border: none;
  padding: 5px 0;
  color: var(--fp-brand);
  font-family: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  transition: var(--fp-transition);
}
.fp-linkbtn:hover { filter: brightness(1.2); }

/* ── Biggest movers rows ─────────────────────────────────────────────── */
.fp-mover {
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr) auto auto 54px;
  align-items: center;
  gap: var(--fp-2);
  padding: 9px 0;
  border-bottom: 1px solid var(--fp-border-soft);
  cursor: pointer;
}
.fp-mover:last-child { border-bottom: none; }
.fp-mover:hover { background: rgba(255,255,255,.02); }
.fp-mover-name { font-weight: 700; font-size: 13px; color: var(--fp-text); }
.fp-mover-ticker { color: var(--fp-text-faint); font-size: 11px; margin-left: 5px; font-weight: 600; }
.fp-mover-price { font-size: 12px; color: var(--fp-text-body); white-space: nowrap; }
.fp-mover-change { font-size: 12px; font-weight: 700; white-space: nowrap; }
.fp-mover img, .fp-mover .coin-initial { width: 24px; height: 24px; border-radius: 50%; }

/* ── AI card ─────────────────────────────────────────────────────────── */
/* The orb is the one purely decorative element carried over from the target.
   Pure CSS — no image request for a 34px circle. */
.fp-orb {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  flex-shrink: 0;
  background:
    radial-gradient(circle at 32% 30%, rgba(255,255,255,.55), transparent 42%),
    linear-gradient(140deg, #7d6cf0, #2b7fd4 55%, #23c2a0);
  box-shadow: 0 0 18px rgba(80,120,220,.35);
}
.fp-ask {
  display: flex;
  align-items: center;
  gap: var(--fp-2);
  margin-top: var(--fp-3);
  padding: 6px 6px 6px 14px;
  border: 1px solid var(--fp-border-control);
  border-radius: var(--fp-radius);
  background: var(--fp-surface-sunk);
}
.fp-ask input {
  flex: 1;
  min-width: 0;
  margin: 0;
  padding: 8px 0;
  background: transparent;
  border: none;
  color: var(--fp-text);
  font-family: inherit;
  font-size: 13px;
}
.fp-ask input:focus { outline: none; }
.fp-ask:focus-within { border-color: var(--fp-brand-40); }
.fp-ask-send {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  border: none;
  border-radius: 50%;
  background: var(--fp-brand);
  color: var(--fp-brand-ink);
  cursor: pointer;
  transition: var(--fp-transition);
}
.fp-ask-send:hover { filter: brightness(1.08); }
.fp-ask-send:disabled { opacity: .5; cursor: not-allowed; }

/* ── Track record on Home ────────────────────────────────────────────── */
.fp-track-body {
  display: grid;
  grid-template-columns: minmax(240px, 340px) minmax(0, 1fr);
  gap: var(--fp-4);
  align-items: start;
}
@media (max-width: 820px) { .fp-track-body { grid-template-columns: 1fr; } }

.fp-track-stats { display: flex; align-items: center; gap: var(--fp-3); flex-wrap: wrap; }
.fp-donut { position: relative; width: 130px; height: 130px; flex-shrink: 0; }
.fp-donut svg { width: 130px; height: 130px; transform: rotate(-90deg); }
.fp-donut-track, .fp-donut-fill { fill: none; stroke-width: 14; }
.fp-donut-track { stroke: var(--fp-loss); }
.fp-donut-fill {
  stroke: var(--fp-gain);
  stroke-dasharray: 289;         /* 2πr, r = 46 */
  stroke-dashoffset: 289;
  transition: stroke-dashoffset var(--fp-motion) var(--fp-ease);
}
.fp-donut-centre {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.fp-donut-value { font-size: 24px; font-weight: 800; line-height: 1; color: var(--fp-text); }
.fp-donut-label { font-size: 10px; color: var(--fp-text-faint); margin-top: 3px; }

.fp-track-counts { margin: 0; display: grid; gap: 9px; }
.fp-track-counts > div { display: flex; align-items: baseline; justify-content: space-between; gap: var(--fp-3); min-width: 150px; }
.fp-track-counts dt { color: var(--fp-text-muted); font-size: 12px; }
.fp-track-counts dd { margin: 0; font-weight: 700; font-size: 15px; }

.fp-signal-row {
  display: grid;
  /* Third column widened for "24h move" — at 62px the header wrapped onto two
     lines and pushed the row taller than the ones around it. */
  grid-template-columns: minmax(0, 1fr) 74px 82px;
  align-items: center;
  gap: var(--fp-2);
  padding: 8px 0;
  border-bottom: 1px solid var(--fp-border-soft);
  font-size: 12px;
}
.fp-signal-row:last-child { border-bottom: none; }
.fp-signal-row .fp-signal-name { color: var(--fp-text); font-weight: 600; }
.fp-signal-head {
  font-size: var(--fp-size-caption);
  letter-spacing: var(--fp-track-label);
  text-transform: uppercase;
  color: var(--fp-text-muted);
  font-weight: 700;
  border-bottom-color: var(--fp-border);
}
.fp-donut-label { line-height: 1.25; }

/* Quick tips lived here. Removed with the card in the 1.2 Today pass — all four
   of its destinations (watchlist, Learn, Community, Ask AI) are permanent nav
   items, so it was a second route to four places you can already see. */

/* ── Notifications on Today ──────────────────────────────────────────────
 * #alertsSection is the full alerts console on /alerts-view and the vision's
 * "Notifications — latest important alerts" on Today. The sort tabs belong to
 * the console: Recent / By Priority / Major Moves are controls for working
 * THROUGH a feed, and Today is not where you work through a feed. Hidden here
 * rather than removed, because the same markup is the console one view away.
 * The list, the summary line, the title and the count are switched in
 * renderAlerts() against the same currentView. */
body.fp-view-home #alertsSection .alerts-tabs { display: none; }
body.fp-view-home #alertsSection .disclaimer { margin-top: var(--fp-2); }

/* ── Nav: the "More" disclosure, secondary group and promo ───────────── */
.sidebar-more {
  margin-top: var(--fp-3);
  padding-top: var(--fp-2);
  border-top: 1px solid var(--fp-border-soft);
}
.sidebar-more > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--fp-2);
  padding: 10px 13px;
  border-radius: var(--fp-radius);
  color: var(--fp-text-muted);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  list-style: none;
  transition: var(--fp-transition);
}
.sidebar-more > summary::-webkit-details-marker { display: none; }
.sidebar-more > summary:hover { color: var(--fp-text); background: rgba(255,255,255,.03); }
.sidebar-more > summary svg { width: 15px; height: 15px; transition: transform var(--fp-motion) var(--fp-ease); }
.sidebar-more[open] > summary svg { transform: rotate(180deg); }

.sidebar-secondary .nav-item { font-size: 13px; opacity: .82; }
.sidebar-secondary .nav-item:hover { opacity: 1; }
.sidebar-more--links .sidebar-links { border-top: none; padding-top: var(--fp-1); }

.fp-nav-promo {
  margin-top: var(--fp-4);
  padding: var(--fp-3);
  background: var(--fp-surface-sunk);
  border: 1px solid var(--fp-border);
  border-radius: var(--fp-radius);
}
.fp-nav-promo-head {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 13px;
  font-weight: 700;
  color: var(--fp-text);
  margin-bottom: var(--fp-2);
}
.fp-nav-promo-body { font-size: 12px; line-height: 1.5; color: var(--fp-text-muted); }

/* ── Coin detail sheet (mobile) ──────────────────────────────────────────
 * The plan's preferred fix for item 7: details appear ON DEMAND when a coin is
 * tapped, rather than living permanently in the page as a card showing a
 * placeholder prompt.
 *
 * It was an inline section that the page scrolled to. That has two problems:
 * the panel occupies the page whether or not you have chosen a coin, and after
 * the scroll you have lost your place in the list you were reading. A sheet
 * costs nothing when closed and returns you exactly where you were.
 *
 * Desktop is untouched — it keeps the sticky right drawer, which is already the
 * on-demand pattern at that width.
 *
 * Everything below is inside the mobile media query, so the desktop cascade
 * cannot be affected by it at all. */
@media (max-width: 1024px) {
  .fp-sheet {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 500;                    /* above the bottom navbar (420) */
    max-height: 85vh;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    margin: 0;
    border-radius: var(--fp-radius-lg) var(--fp-radius-lg) 0 0;
    border: 1px solid var(--fp-border);
    border-bottom: none;
    background: var(--fp-surface);
    box-shadow: 0 -12px 40px rgba(0,0,0,.6);
    transform: translateY(100%);
    transition: transform var(--fp-motion) var(--fp-ease);
    /* Clear the fixed bottom bar and the phone's home indicator. */
    padding-bottom: calc(var(--fp-6) + env(safe-area-inset-bottom));
  }
  .fp-sheet.fp-sheet--open { transform: translateY(0); }

  /* The sheet header: grab handle centred, close button on the right. Sticky
     lives HERE now rather than on the handle, so both controls stay pinned as
     the sheet's content scrolls — a close button that scrolls away is the bug
     this is fixing, one step removed. */
  .fp-sheet-head {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--fp-surface);
  }

  /* A grab handle, because a sheet that can be dragged down looks draggable.
     Tapping it closes — the same affordance without requiring a gesture.
     Full width and centred; the close button is taken out of flow rather than
     sharing a grid row with it, so the pill sits on the sheet's true centre
     instead of the centre of whatever space the button left over. */
  .fp-sheet-handle {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--fp-2);
    width: 100%;
    padding: 10px 0 12px;
    border: none;
    background: transparent;
    cursor: pointer;
  }

  /* The explicit way out. 36x36 — comfortably past the 24px AA floor and sized
     for a thumb, since this is the control someone reaches for when they have
     opened a coin by accident. */
  .fp-sheet-close {
    position: absolute;
    top: 4px;
    right: 8px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border: 1px solid var(--fp-border);
    border-radius: 50%;
    background: rgba(255, 255, 255, .04);
    color: var(--fp-text-body);
    cursor: pointer;
    transition: var(--fp-transition);
  }
  .fp-sheet-close:hover,
  .fp-sheet-close:active {
    color: var(--fp-text);
    border-color: var(--fp-brand-30);
  }
  .fp-sheet-close svg { width: 18px; height: 18px; }
  .fp-sheet-handle::before {
    content: "";
    width: 42px;
    height: 4px;
    border-radius: var(--fp-radius-pill);
    background: var(--fp-border-strong);
  }

  .fp-sheet-backdrop {
    position: fixed;
    inset: 0;
    z-index: 499;
    background: rgba(0,0,0,.55);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--fp-motion) var(--fp-ease);
  }
  .fp-sheet-backdrop.fp-sheet--open { opacity: 1; pointer-events: auto; }

  /* The page must not scroll behind an open sheet. */
  body.fp-sheet-lock { overflow: hidden; }
}

/* ── Canvas guard ────────────────────────────────────────────────────────
 * Chart.js writes an explicit pixel width onto its canvas, sized from the
 * container at creation time. The BTC-dominance chart is created after a lazy
 * library load, so at 320px it was being handed a width measured before layout
 * had settled — it came out 331px, which is wider than the phone. That widened
 * the layout viewport itself (innerWidth reported 331 against a 320 client
 * width), so the whole page scrolled sideways by 11px and the fixed bottom
 * navbar stretched with it. The navbar looked like the culprit and was not.
 *
 * max-width caps the rendered box whatever the attribute says. Measured
 * pre-existing on production, at 320 only. */
canvas { max-width: 100%; }

/* ── Retry: an error that offers the user a way out ──────────────────── */
.fp-retry {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--fp-2);
  flex-wrap: wrap;
  padding: var(--fp-3);
  color: var(--fp-text-muted);
  font-size: 13px;
}

/* ── Intelligence: the "why this signal" hero ───────────────────────────
 * 1.2, 6 August. Intelligence's own subheading promises "why a signal fired,
 * and what moved behind it", and the page had no surface that answered it.
 * This is that surface: one hero per page, per the vision.
 *
 * Nothing here invents a colour. Direction is --fp-gain / --fp-loss, the
 * accent is --fp-brand, and the panel chrome is the same surfaces, hairlines
 * and radii every other card uses — a new page should be recognisable as the
 * same product before it is recognisable as new.
 */
.fp-why-head {
  display: flex;
  align-items: flex-start;
  gap: var(--fp-3);
  flex-wrap: wrap;
}
.fp-why-logo { width: 48px; height: 48px; flex-shrink: 0; }
.fp-why-logo img, .fp-why-logo .coin-initial { width: 48px; height: 48px; border-radius: 50%; }
.fp-why-id { flex: 1 1 200px; min-width: 0; }

/* The eyebrow states WHY this coin is the one on screen — "score rose 12
 * points, 8m ago" — rather than leaving the reader to assume it is arbitrary.
 * That sentence is the whole reason the hero picks a coin at all. */
.fp-why-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fp-size-caption);
  font-weight: 700;
  letter-spacing: var(--fp-track-label);
  text-transform: uppercase;
  color: var(--fp-brand);
  margin-bottom: var(--fp-2);
}
.fp-why-eyebrow svg { width: 13px; height: 13px; }
.fp-why-name { font-size: 22px; font-weight: 900; line-height: 1.1; color: var(--fp-text); }
.fp-why-name span { color: var(--fp-text-muted); font-size: 14px; font-weight: 600; }
.fp-why-price {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-top: var(--fp-1);
  font-feature-settings: var(--fp-num);
}
.fp-why-price strong { font-size: 24px; font-weight: 900; color: var(--fp-text); }
.fp-why-price span { font-size: 15px; font-weight: 700; }

/* Score and confidence sit together: the number and the word for the number. */
.fp-why-score {
  display: flex;
  gap: var(--fp-5);
  flex-shrink: 0;
  padding-left: var(--fp-3);
  border-left: 1px solid var(--fp-border);
}
.fp-why-score > div { min-width: 0; }
.fp-why-score dt {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: var(--fp-track-label);
  text-transform: uppercase;
  color: var(--fp-text-muted);
}
.fp-why-score dd {
  margin: 5px 0 0 0;
  font-size: 22px;
  font-weight: 900;
  color: var(--fp-text);
  font-feature-settings: var(--fp-num);
}

.fp-why-block { margin-top: var(--fp-4); }
.fp-why-block h3 {
  margin: 0 0 var(--fp-2) 0;
  font-size: var(--fp-size-h3);
  line-height: var(--fp-lh-h3);
  color: var(--fp-text);
}

/* Drivers are numbered, because "the top three things moving this" is a
 * ranking and a bulleted list hides that it is one. */
.fp-why-drivers { list-style: none; margin: 0; padding: 0; counter-reset: fpdrv; }
.fp-why-drivers li {
  counter-increment: fpdrv;
  display: flex;
  gap: var(--fp-2);
  align-items: flex-start;
  padding: 9px 0;
  border-bottom: 1px solid var(--fp-border-soft);
  color: var(--fp-text-body);
  font-size: var(--fp-size-body);
  line-height: var(--fp-lh-body);
}
.fp-why-drivers li:last-child { border-bottom: none; }
.fp-why-drivers li::before {
  content: counter(fpdrv);
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  margin-top: 2px;
  border-radius: 50%;
  background: var(--fp-brand-12);
  color: var(--fp-brand);
  font-size: 11px;
  font-weight: 800;
  text-align: center;
  line-height: 20px;
  font-feature-settings: var(--fp-num);
}

/* What would weaken it. Set apart from the drivers on purpose: a signal that
 * only ever lists its supporting evidence is a sales pitch, not an
 * explanation. */
.fp-why-invalidation {
  display: flex;
  gap: var(--fp-2);
  padding: 12px 14px;
  border-radius: var(--fp-radius-sm);
  background: rgba(255,255,255,.02);
  border-left: 3px solid var(--fp-text-faint);
  color: var(--fp-text-body);
  font-size: 13px;
  line-height: var(--fp-lh-body);
}
.fp-why-invalidation svg { width: 15px; height: 15px; flex-shrink: 0; margin-top: 3px; color: var(--fp-text-muted); }

/* Score movements — "confidence changes" in the vision's words. */
.fp-why-trail { display: grid; gap: 2px; }
.fp-why-trail-row {
  display: flex;
  align-items: baseline;
  gap: var(--fp-2);
  padding: 8px 0;
  border-bottom: 1px solid var(--fp-border-soft);
  font-size: 13px;
  color: var(--fp-text-body);
}
.fp-why-trail-row:last-child { border-bottom: none; }
.fp-why-trail-move { font-weight: 800; white-space: nowrap; font-feature-settings: var(--fp-num); }
.fp-why-trail-reason { flex: 1 1 auto; min-width: 0; }
.fp-why-trail-when { color: var(--fp-text-faint); font-size: 11px; white-space: nowrap; }

.fp-why-actions { display: flex; gap: var(--fp-2); flex-wrap: wrap; margin-top: var(--fp-4); }

/* ── Major moves ────────────────────────────────────────────────────────
 * The row takes its colour from the DIRECTION of the move. The old marker for
 * this class of event was a hardcoded #0096ff blue, which is neither in the
 * palette nor a colour this product uses; the bolt and the label already say
 * "big", so the colour is free to say "up" or "down" instead. */
.fp-major-row {
  display: flex;
  align-items: center;
  gap: var(--fp-3);
  padding: 11px 0;
  border-bottom: 1px solid var(--fp-border-soft);
}
.fp-major-row:last-child { border-bottom: none; }
.fp-major-bolt {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  flex-shrink: 0;
  border-radius: 50%;
  background: rgba(255,255,255,.04);
}
.fp-major-bolt svg { width: 15px; height: 15px; }
.fp-major-body { flex: 1 1 auto; min-width: 0; }
.fp-major-coin { font-size: 13px; font-weight: 800; color: var(--fp-text); }
.fp-major-msg { font-size: 12px; line-height: var(--fp-lh-body); color: var(--fp-text-muted); }
.fp-major-right { text-align: right; flex-shrink: 0; }
.fp-major-move { font-size: 14px; font-weight: 800; font-feature-settings: var(--fp-num); }
.fp-major-when { font-size: 11px; color: var(--fp-text-faint); }

@media (max-width: 600px) {
  .fp-why-score {
    padding-left: 0;
    border-left: none;
    border-top: 1px solid var(--fp-border);
    padding-top: var(--fp-3);
    width: 100%;
  }
  .fp-why-logo, .fp-why-logo img, .fp-why-logo .coin-initial { width: 40px; height: 40px; }
  .fp-why-name { font-size: 19px; }
  .fp-why-price strong { font-size: 21px; }
}

/* ── AI Assistant page ──────────────────────────────────────────────────
 * 1.2, 6 August. The vision asks for a dedicated conversation page and rules
 * out market tables on it, so there is no grid here — a thread, prompts to
 * start one, and the composer.
 *
 * The thread scrolls inside its own box rather than growing the page. A long
 * conversation that pushes the input off the bottom of a phone is the failure
 * mode this shape exists to avoid.
 */
.fp-assistant-thread {
  max-height: 52vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  margin-bottom: var(--fp-3);
}
.fp-assistant-thread .fp-empty { padding: var(--fp-5) var(--fp-3); }
.fp-assistant-thread .fp-orb { margin: 0 auto var(--fp-3); display: block; }

.fp-assistant-turn { margin-bottom: var(--fp-4); }
.fp-assistant-turn:last-child { margin-bottom: 0; }

/* The question is a label on the answer, not a peer of it — one voice should
   dominate, and it is the one that did the explaining. */
.fp-assistant-q {
  display: flex;
  gap: var(--fp-2);
  align-items: baseline;
  margin-bottom: var(--fp-2);
  color: var(--fp-text-muted);
  font-size: 13px;
  line-height: var(--fp-lh-body);
}
.fp-assistant-q span {
  flex-shrink: 0;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: var(--fp-track-label);
  text-transform: uppercase;
  color: var(--fp-text-faint);
}

.fp-assistant-a {
  padding: var(--fp-3) 14px;
  border-radius: var(--fp-radius-sm);
  background: var(--fp-brand-08);
  border: 1px solid var(--fp-brand-30);
  color: var(--fp-text-body);
  font-size: 13px;
  line-height: 1.65;
  white-space: pre-wrap;
}
.fp-assistant-a--pending {
  background: rgba(255,255,255,.02);
  border-color: var(--fp-border);
  color: var(--fp-text-muted);
}
.fp-assistant-a--error {
  background: rgba(var(--fp-loss-rgb), .06);
  border-color: rgba(var(--fp-loss-rgb), .25);
  white-space: normal;
}

/* Suggested prompts. Real questions about real coins — see
   assistantSuggestedPrompts() for why they are not a static list. */
.fp-assistant-prompts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--fp-2);
  margin-bottom: var(--fp-3);
}
.fp-chip {
  padding: 8px 13px;
  border-radius: var(--fp-radius-pill);
  background: rgba(255,255,255,.03);
  border: 1px solid var(--fp-border);
  color: var(--fp-text-body);
  font-family: inherit;
  font-size: 12.5px;
  line-height: 1.3;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--fp-motion) var(--fp-ease), color var(--fp-motion) var(--fp-ease);
}
.fp-chip:hover { border-color: var(--fp-brand-40); color: var(--fp-text); }
.fp-chip:focus-visible { outline: var(--fp-focus); outline-offset: var(--fp-focus-offset); }

.fp-assistant-ask { width: 100%; }

/* The header's "Ask AI anything" button opens the coin-search overlay, which is
   how the assistant was reached before it had a page. On the assistant page
   itself it points at a worse version of where you already are — and it sits
   directly above a composer that does the same job better. Hidden here only;
   `fp-view-assistant` is the body class applyView() already maintains, so this
   needs no JavaScript. */
body.fp-view-assistant .fp-home-actions > .fp-btn { display: none; }

.fp-assistant-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--fp-2);
  flex-wrap: wrap;
  margin-top: var(--fp-2);
  min-height: 18px;
}

/* The tier wall, stated before anyone types into it rather than after. */
.fp-assistant-tier {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--fp-3);
  flex-wrap: wrap;
  padding: 14px 16px;
  margin-bottom: var(--fp-4);
  border-radius: var(--fp-radius);
  background: rgba(255,255,255,.02);
  border: 1px solid var(--fp-border);
}
.fp-assistant-tier-title { font-size: 14px; font-weight: 800; color: var(--fp-text); }
.fp-assistant-tier-body {
  margin-top: var(--fp-1);
  max-width: 62ch;
  font-size: 13px;
  line-height: var(--fp-lh-body);
  color: var(--fp-text-muted);
}
.fp-assistant-tier-body strong { color: var(--fp-text-body); }
.fp-assistant-tier-actions { display: flex; gap: var(--fp-2); flex-wrap: wrap; flex-shrink: 0; }
/* The "See plans" action is an <a>, so it needs the button's text treatment. */
.fp-assistant-tier-actions a.fp-btn { text-decoration: none; }

@media (max-width: 600px) {
  .fp-assistant-thread { max-height: 46vh; }
  .fp-assistant-tier-actions { width: 100%; }
  .fp-assistant-tier-actions .fp-btn { flex: 1 1 auto; justify-content: center; }
}

/* ── Profile page ───────────────────────────────────────────────────────
 * 1.2, 6 August. The vision's "settings, subscription, notifications, theme,
 * account", as a view rather than a modal.
 *
 * The preference controls below the account are the EXISTING #settingsSection,
 * tagged into this view rather than restyled — a settings panel that looks
 * different depending on which door you came through is worse than one that
 * looks the same everywhere.
 */
.fp-profile-id {
  display: flex;
  align-items: center;
  gap: var(--fp-3);
  flex-wrap: wrap;
}
.fp-profile-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 46px;
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--fp-brand-12);
  border: 1px solid var(--fp-brand-30);
  color: var(--fp-brand);
  font-size: 19px;
  font-weight: 800;
  text-transform: uppercase;
}
.fp-profile-who { flex: 1 1 200px; min-width: 0; }
.fp-profile-name {
  font-size: 17px;
  font-weight: 800;
  color: var(--fp-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.fp-profile-email {
  margin-top: 2px;
  font-size: 13px;
  color: var(--fp-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.fp-profile-badges { display: flex; gap: var(--fp-2); flex-wrap: wrap; flex-shrink: 0; }

.fp-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 10px;
  border-radius: var(--fp-radius-pill);
  background: rgba(255,255,255,.04);
  border: 1px solid var(--fp-border);
  color: var(--fp-text-body);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: var(--fp-track-label);
  text-transform: uppercase;
  white-space: nowrap;
}
.fp-badge svg { width: 12px; height: 12px; }
.fp-badge--ok   { color: var(--fp-gain); border-color: var(--fp-brand-30); background: var(--fp-brand-08); }
/* Unverified is a genuine warning — some actions are refused until it clears —
   so it wears --fp-warn, which is what that token is for. */
.fp-badge--warn { color: var(--fp-warn); border-color: rgba(255,204,51,.30); background: rgba(255,204,51,.07); }

.fp-profile-actions {
  display: flex;
  gap: var(--fp-2);
  flex-wrap: wrap;
  margin-top: var(--fp-3);
}
.fp-profile-actions a.fp-btn { text-decoration: none; }

.fp-profile-divider {
  height: 1px;
  margin: var(--fp-4) 0;
  background: var(--fp-border);
}

.fp-profile-signedout .fp-body { color: var(--fp-text-body); }

/* Profile shows the settings panel inline, so its close × has nothing to close
   back to — the panel IS the page here. Hidden on this view only; on the
   `controls` view it is still the way out. `fp-view-profile` is the body class
   applyView() already maintains. */
body.fp-view-profile #settingsSection > button[aria-label="Close settings"] { display: none; }

/* The app-wide "verify your email" banner renders on every view. On Profile it
   is the third thing on one screen saying the same sentence — there is already
   an EMAIL UNVERIFIED badge and a line naming the address with its own Resend
   button, both in the context of the account they are about. The banner is the
   nag for everywhere else; here the page says it better.
   `!important` is load-bearing: renderVerifyBanner() sets display:flex as an
   inline style, and an inline style beats a stylesheet rule without it. */
body.fp-view-profile #verifyBanner { display: none !important; }

@media (max-width: 600px) {
  .fp-profile-badges { width: 100%; }
  .fp-profile-actions .fp-btn { flex: 1 1 auto; justify-content: center; }
}

/* "Sign up free" with "Why should I sign up?" underneath it (Ryan, 7 Aug).
   Stacked and centred so the link reads as a footnote to the button rather
   than a competing action — the landing page answers the question the CTA
   raises, and a second button beside it would just split the click. */
.fp-personalise-cta {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--fp-1);
  flex-shrink: 0;
}
.fp-personalise-cta .fp-linkbtn { font-size: 12px; }

/* ── Colour-vision modes ────────────────────────────────────────────────
 * Ryan's call, 7 August. Opt-in, one per condition, set in Profile.
 *
 * WHAT THE MEASUREMENTS ACTUALLY SHOWED, because it changed the design:
 * our green and red are ALREADY well separated for colour-blind users —
 * perceptual distance (CIE deltaE) between them after simulation is 132
 * (deuteranopia), 92 (protanopia), 115 (tritanopia), where 20 is "obvious".
 * Nobody is unable to tell up from down. What is wrong is subtler and worth
 * fixing anyway:
 *   - the colours do not MEAN what they look like. To a deuteranope our green
 *     reads as purple-blue and our red as olive, so "green = good" is a
 *     convention they cannot use, only memorise.
 *   - the genuinely weak pair is AMBER against RED — deltaE 16 under
 *     deuteranopia, the closest two semantic colours in the product.
 *
 * So each mode picks a pair that survives THAT person's vision as natural
 * opposites, and pulls amber clear of red. Blue/orange measures 160 for
 * up/down and lifts amber/red from 16 to 25 — the pair that was actually at
 * risk.
 *
 * YES, THIS INTRODUCES BLUE, and only here. It is not a brand decision: to
 * someone in this mode our green already looks blue-purple, so there is no
 * green identity to protect on their screen. --fp-brand does NOT move — the
 * wordmark, buttons and chrome stay green for everyone. Only the three
 * SEMANTIC colours change, which is exactly what --fp-gain existing as its own
 * name was for.
 *
 * This costs three declarations per mode because every gain/loss/warn in the
 * product resolves through these tokens. Before the 6 August sweep it would
 * have been 39 files.
 */
:root[data-colour-vision="deuteranopia"],
:root[data-colour-vision="protanopia"] {
  --fp-gain:      #3d8bff;
  --fp-gain-rgb:  61,139,255;
  --fp-loss:      #ff7a1a;
  --fp-loss-rgb:  255,122,26;
  --fp-warn:      #ffe066;
  /* The strength ramp's middle step is a dimmed brand green, which this mode's
     user cannot separate from the brand. It follows the gain hue instead so the
     ramp still reads as one family. */
  --fp-strength-mid: #4a7fb5;
}

/* Blue-yellow. Red and green are fine here — the pair that collapses is amber
   against red, because amber simulates to pink. Only the warning colour moves. */
:root[data-colour-vision="tritanopia"] {
  --fp-warn:      #ffffff;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * ACCESSIBILITY — the parts that are not a colour
 *
 * Lives in the token layer, not in a numbered dashboard layer, because every
 * page in the product loads this file and only the dashboard loads the rest.
 * A focus ring that works on the dashboard and nowhere else is not a focus
 * ring, it is a coincidence.
 * ══════════════════════════════════════════════════════════════════════════ */

/* SKIP LINK. Off-screen until focused, then a real button at the top-left.
   It is the first thing in the tab order, so the rail — eight destinations,
   two disclosures and an account chip, all focusable — stops being a toll gate
   on the way to the market data. */
.fp-skip {
  position: absolute;
  left: var(--fp-2);
  top: var(--fp-2);
  z-index: 100000;
  padding: 10px 16px;
  border-radius: 8px;
  background: var(--fp-brand);
  color: #04120b;
  font-weight: 700;
  font-size: 14px;
  text-decoration: none;
  transform: translateY(-200%);
}
.fp-skip:focus {
  transform: translateY(0);
}

/* FOCUS. There was no focus styling anywhere in the product — nothing removed
   the ring either, so keyboard users had the browser default: a thin
   black-on-white pair drawn for a light page, over a near-black one.
   :focus-visible rather than :focus, so a mouse click on a card does not leave
   a ring behind it; the brand green is already the "this is active" colour
   everywhere else, so the ring says the same thing the rest of the UI does. */
:where(a, button, input, select, textarea, summary, [tabindex], [role="button"]):focus-visible {
  outline: 2px solid var(--fp-brand);
  outline-offset: 2px;
  /* No border-radius here on purpose. An outline already follows the element's
     own radius, and setting one would reshape inputs and selects while focused
     — a control that changes shape when you tab to it. */
}

/* A row is a big target and a 2px ring on it is easy to lose against the card
   edge; these get the ring plus a tint so the focused row is unmistakable. */
.fp-mover:focus-visible,
.coin-card:focus-visible,
.signal-table tbody tr:focus-visible {
  outline: 2px solid var(--fp-brand);
  outline-offset: -2px;
  background: rgba(var(--fp-brand-rgb), .08);
}

/* The skip target itself is focused programmatically (tabindex="-1") and must
   NOT draw a ring — nothing was navigated to, the page simply moved. */
#mainContent:focus,
#mainContent:focus-visible {
  outline: none;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * SELF-REFERENTIAL CONTROLS
 *
 * A card that appears on two views carries a "go to the full thing" link that
 * is only meaningful on one of them. On the other it points at the view you
 * are already reading, and a control that does nothing when clicked is worse
 * than no control: it reads as broken navigation.
 *
 * Both cases are hidden by view rather than by rewriting the markup, because
 * the same card genuinely wants the link on its other view.
 *   - #watchlistPanel is on Home and Watchlist. "Open watchlist" is right on
 *     Home, dead on Watchlist.
 *   - #trackRecordHome carries "View full track record" and is tagged
 *     data-view="trackrecord", so its link has NEVER been reachable from
 *     anywhere but the page it points at. That tag is the real bug — the card
 *     was written for Today — but hiding a dead control is correct either way,
 *     and where the card belongs is a layout decision rather than a fix.
 * ══════════════════════════════════════════════════════════════════════════ */
body.fp-view-watchlist #watchlistPanel > .fp-card-head .fp-linkbtn,
body.fp-view-trackrecord #trackRecordHome > .fp-card-head .fp-linkbtn {
  display: none;
}

/* Two cards titled "Watchlist", stacked, on the Watchlist view — the summary
   card and the list itself, plus the page's own <h1>. Three identical words
   down one screen. The summary card keeps the title; the list below it reads
   as what it obviously is. */
body.fp-view-watchlist .fp-wl-list-title {
  display: none;
}
/* Restores the margin .fp-card-title does not carry — that class is written for
   a .fp-card-head flex row, and this heading stands on its own. */
.fp-wl-list-title { margin: 0 0 14px 0; }
