/* =============================================
   PULSE — PWA styles
   The timing tower: a race broadcast's live graphics for the things you
   watch. Near-black ground, flat bars split by hairlines, one face
   (Titillium Web) doing everything from italic positions to tabular
   readouts. Colour has fixed jobs: white acts, yellow is the interval to a
   buzz, green and red are direction, purple is a buzz, orange is caution.
   No glow, no tints, no bloom.
   Mobile-first: 44px minimum touch targets, 16px inputs (iOS refuses to
   focus anything smaller without zooming), safe-area padding on all four
   edges, and press states instead of hover states.
   ============================================= */

/* Self-hosted, Latin subset (fonts/OFL-TitilliumWeb.txt): the weights the
   tower uses, plus the one real italic for positions and the wordmark. */
@font-face { font-family: 'Titillium Web'; font-style: normal; font-weight: 400; font-display: swap; src: url('fonts/titillium-400.woff2') format('woff2'); }
@font-face { font-family: 'Titillium Web'; font-style: normal; font-weight: 600; font-display: swap; src: url('fonts/titillium-600.woff2') format('woff2'); }
@font-face { font-family: 'Titillium Web'; font-style: normal; font-weight: 700; font-display: swap; src: url('fonts/titillium-700.woff2') format('woff2'); }
@font-face { font-family: 'Titillium Web'; font-style: italic; font-weight: 700; font-display: swap; src: url('fonts/titillium-700-italic.woff2') format('woff2'); }
@font-face { font-family: 'Titillium Web'; font-style: normal; font-weight: 900; font-display: swap; src: url('fonts/titillium-900.woff2') format('woff2'); }

:root {
  /* White is the action colour on the dark broadcast: the TRACK key, the
     switch, the focus ring. Light mode flips it to near-black. */
  --accent-rgb: 244, 244, 247;
  --accent: rgb(var(--accent-rgb));
  --accent-text: #f4f4f7;
  --accent-dim: rgba(255, 255, 255, .08);
  --border: #23232f;      /* the hairline between bars */
  --border2: #36364c;     /* emphasis edges: open sheet, focus-within */

  --bg: #0a0a0e;          /* the broadcast black */
  --bg2: #111118;         /* the tower */
  --bg3: #17171f;         /* an open bar, a field */
  --bg4: #21212d;         /* wells: chart band, stepper, unlit tags */
  --text: #f4f4f7;
  --text2: #a8a8bd;
  --text3: #9090a8;       /* 5.6:1 on the tower, still quieter than text2 */

  /* Fixed roles. Direction is data, never an action colour. */
  --up: #35d07f;      --up-dim: rgba(53, 208, 127, .14);
  --down: #ff4b55;    --down-dim: rgba(255, 75, 85, .12);
  --gap: #ffd23f;         /* the interval to a buzz */
  --buzz: #8b3fe0;        /* a buzz: the fastest-lap colour, dark enough for white type */
  --amber: #ff9a3c;       /* caution: late checks, alerts off, a full plan */
  --on-amber: #0a0a0e;

  /* Sources as small team chips. Kept clear of the role colours. */
  --poly: #4da3ff;
  --kalshi: #2ec4b6;
  --stock: #c9c9d6;

  --topbar-bg: #0a0a0e;   /* solid: no blur behind it, so nothing may show through */
  --sheet-bg: rgba(17, 17, 24, .98);
  /* Floating layers get a short, offset shadow, never a haze */
  --shadow: 0 1px 2px rgba(0, 0, 0, .5), 0 4px 8px -2px rgba(0, 0, 0, .55);
  --shadow-lift: 0 -1px 2px rgba(0, 0, 0, .35), 0 -4px 8px -2px rgba(0, 0, 0, .45);
  --ink: #0a0a0e;         /* text on the white action fill */
  --ink-danger: #0a0a0e;
  --ink-up: #0a0a0e;

  --display: 'Titillium Web', system-ui, sans-serif;
  --brand: var(--display);
  --mono: var(--display);   /* numbers are the display face, set tabular */
  --radius: 8px;
  --radius-sm: 6px;
  --tap: 44px;            /* iOS HIG minimum; Android gets 48 (see .platform-android) */

  --safe-b: env(safe-area-inset-bottom);
  --kb: 0px;              /* on-screen keyboard overlap, set from app.js */

  /* Behind a sheet or dialog: the room dims, in either theme */
  --scrim: rgba(4, 4, 8, .66);

  /* The app icon as the phone shows it: its own dark tile and neon mark in
     either theme, because the icon itself doesn't change with the app's */
  --icon-tile: #14101d;
  --icon-mark: #3cd8e3;
}

/* Light theme — the same broadcast with the lights on: white bars on a cool
   grey floor, near-black type and a near-black action key. Role colours
   deepen until they read as text on white. */
:root[data-theme="light"] {
  --accent-rgb: 10, 10, 14;
  --accent-text: #0a0a0e;
  --accent-dim: rgba(10, 10, 14, .06);
  --border: #e1e3ea;
  --border2: #c3c6d2;

  --bg: #eceef2;
  --bg2: #ffffff;
  --bg3: #f6f7fa;
  --bg4: #e2e4eb;
  --text: #0a0a0e;
  --text2: #474a59;
  --text3: #5b5e6e;

  --up: #0c7a44;      --up-dim: rgba(12, 122, 68, .10);
  --down: #c4202f;    --down-dim: rgba(196, 32, 47, .09);
  --gap: #876200;     /* the interval's yellow, deep enough for 5:1 on white */
  --buzz: #7a2fd6;
  --amber: #a64b00;
  --on-amber: #ffffff;

  --poly: #1a5fd0;
  --kalshi: #0b8a7f;
  --stock: #6b6e7e;

  --topbar-bg: #eceef2;
  --sheet-bg: rgba(255, 255, 255, .98);
  --shadow: 0 1px 2px rgba(15, 23, 36, .08), 0 4px 8px -2px rgba(15, 23, 36, .12);
  --shadow-lift: 0 -1px 2px rgba(15, 23, 36, .06), 0 -4px 8px -2px rgba(15, 23, 36, .10);

  --ink: #ffffff;         /* white type on the near-black key */
  --ink-danger: #ffffff;
  --ink-up: #ffffff;
}

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

/* Author `display:` rules outrank the UA's `[hidden] { display: none }`, and
   most of what we toggle with `hidden` is a flex container. Settle it once. */
[hidden] { display: none !important; }

html {
  background: var(--bg);
  -webkit-text-size-adjust: 100%;
  /* Own the overscroll so pull-to-refresh is ours, not the browser's */
  overscroll-behavior-y: contain;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--display);
  font-size: 1rem;   /* follows the root, so the phone's text size setting applies */
  /* Numbers are the product: lining figures that line up down the tower */
  font-variant-numeric: lining-nums tabular-nums;
  min-height: 100dvh;
  overscroll-behavior-y: contain;
  -webkit-tap-highlight-color: transparent;
}

/* Chrome is non-selectable; the data users might want to copy is not */
button, .topbar, .sheet-head, .tower-head, .badge {
  -webkit-user-select: none; user-select: none;
}
.item-label, .item-value, .alert-label, .alert-move, .mono {
  -webkit-user-select: text; user-select: text;
}

/* Kill the 300ms tap delay and stop double-tap zoom on controls */
button, input, a, .row, .chip, .seg { touch-action: manipulation; }

button { font: inherit; color: inherit; }

:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 2px; }
::selection { background: color-mix(in srgb, var(--text) 24%, transparent); color: var(--text); }
input { caret-color: var(--text); }

/* Thin scrollbars, the tower's hairline weight */
::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; }

/* ── Topbar ── */
.topbar {
  position: sticky; top: 0; z-index: 30;
  display: grid; grid-template-columns: auto 1fr auto; align-items: center;
  padding: env(safe-area-inset-top) max(.75rem, calc((100% - 680px) / 2 + .25rem)) 0;
  min-height: calc(56px + env(safe-area-inset-top));
  background: var(--topbar-bg);
  border-bottom: 1px solid var(--border);
}
.topbar-side { display: none; }
.brand { display: flex; align-items: center; gap: .5rem; justify-self: start; grid-column: 1; }
.topbar-actions { grid-column: 3; }
/* The broadcast's own mark: heavy italic caps, tight, like a network bug */
.brand-name {
  font-family: var(--brand); font-weight: 700; font-style: italic;
  letter-spacing: .02em; font-size: 1.1rem;
}
/* The logo is also the status light, so the bar carries one element instead
   of a mark plus a pip. Grey until the first successful load. */
.brand-mark {
  width: 22px; height: 22px; flex-shrink: 0;
  fill: none; stroke: var(--text3); stroke-width: 2.1;
  stroke-linecap: round; stroke-linejoin: round;
  transition: stroke .3s, filter .3s;
}
.brand-mark.on  { stroke: var(--text); }
.brand-mark.off { stroke: var(--down); }
/* A test buzz went out: the trace redraws itself once, left to right, and
   lands lit. pathLength="1" on the path makes the dash maths unit-free. */
.brand-mark.credit path { animation: credit 1.1s cubic-bezier(.65, 0, .35, 1) 1; }
@keyframes credit {
  0%   { stroke-dasharray: 1; stroke-dashoffset: 1; }
  100% { stroke-dasharray: 1; stroke-dashoffset: 0; }
}

/* Room for both keys always: the bell comes and goes with push support,
   and the menu button must not move when it does. */
.topbar-actions {
  display: flex; align-items: center; justify-content: flex-end; gap: .15rem; justify-self: end;
  min-width: calc(2 * var(--tap) + .15rem);
}

/* ── Icon buttons ── */
.icon-btn {
  display: grid; place-items: center;
  width: var(--tap); height: var(--tap);
  color: var(--text2); background: none;
  border: none; border-radius: 50%;
  cursor: pointer;
  transition: background .15s, color .15s, transform .1s;
}
.icon-btn svg {
  width: 22px; height: 22px;
  fill: none; stroke: currentColor; stroke-width: 1.9;
  stroke-linecap: round; stroke-linejoin: round;
}
.icon-btn svg circle[r="1.8"] { fill: currentColor; stroke: none; }
.icon-btn:active { background: var(--bg4); transform: scale(.92); }
.icon-btn.on { color: var(--text); }
.icon-btn.small { width: var(--tap); height: var(--tap); }
.icon-btn.small svg { width: 18px; height: 18px; }

/* ── Layout ── */
.shell {
  max-width: 680px; margin: 0 auto;
  padding: 1rem 1rem calc(6.5rem + var(--safe-b));
  display: flex; flex-direction: column; gap: 1rem;
}

.banner {
  font-size: .9rem; line-height: 1.55; color: var(--text2);
  background: var(--bg3); border: 1px solid var(--border);
  border-radius: var(--radius); padding: .9rem 1rem;
}
.banner strong { color: var(--text); }
.banner p { margin-top: .25rem; }
.banner-dismissible { display: flex; align-items: flex-start; gap: .5rem; }
.banner-dismissible > div { flex: 1; }

.panel {
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem;
}
/* Full up. Amber, not red — you've used what you have, nothing is wrong. */
.count.at-cap { color: var(--amber); }

/* "Watching 3 · checked 20s ago": the watcher's pulse, from the server's
   own clock. Amber when checks stall: something to know, not an error. */
/* The live strip: a lit dot while checks arrive, caution orange when they
   stall or when nothing can reach you */
.heartbeat {
  display: flex; align-items: center; gap: .4rem; margin-right: .3rem;
  font-size: .78rem; font-weight: 600; color: var(--text2); white-space: nowrap;
}
.heartbeat::before {
  content: ''; flex-shrink: 0; width: 7px; height: 7px; border-radius: 50%;
  background: var(--up);
}
.heartbeat.late { color: var(--amber); }
.heartbeat.late::before { background: var(--amber); }
/* The fix, inline: a real 44px target that reads as part of the sentence */
/* "Alerts off" is its own fix: the bug turns into a button that says so */
.hb-fix {
  min-height: var(--tap); padding: 0 .2rem;
  font: 700 .78rem var(--display); color: var(--amber);
  background: none; border: none; cursor: pointer;
  text-decoration: underline; text-underline-offset: 3px;
}

.plan-notice {
  margin-top: .7rem;
  font-size: .8rem; line-height: 1.5; color: var(--amber);
  text-align: center;
}

/* Offline: says what's wrong and hands you the retry, instead of a spinner
   that never ends. */
.banner.offline { display: flex; align-items: center; gap: .75rem; }
.banner.offline > div { flex: 1; min-width: 0; }
.banner.offline .ghost-btn { min-height: var(--tap); }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Test buzz offer: the same banner shape as the offline card, but good news */
/* Words and dismiss on top, the action underneath: side by side, the button
   squeezed the sentence to four lines on a 360px phone. */
.banner.test-buzz {
  display: grid; grid-template-columns: 1fr auto; align-items: start; column-gap: .5rem;
  border-color: var(--border2);
}
.banner.test-buzz > div { min-width: 0; }
.banner.test-buzz .icon-btn { grid-column: 2; grid-row: 1; margin: -.5rem -.5rem 0 0; }
.test-buzz-btn {
  grid-column: 1; justify-self: start; margin-top: .7rem;
  min-height: var(--tap);
  font-family: var(--display); font-weight: 700; font-style: italic; font-size: .82rem;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink); background: var(--accent);
  border: none; border-radius: var(--radius-sm);
  padding: 0 .95rem; cursor: pointer;
  transition: transform .1s, opacity .15s;
}
.test-buzz-btn:active { transform: scale(.95); }
.test-buzz-btn:disabled { opacity: .5; cursor: wait; }

/* ── Connecting banner + spinner ── */
.banner.conn { display: flex; align-items: center; gap: .75rem; }
.spinner {
  flex-shrink: 0; width: 16px; height: 16px;
  border: 2px solid var(--border2); border-top-color: var(--accent-text);
  border-radius: 50%;
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ── Pull to refresh ── */
.ptr {
  position: fixed; z-index: 25;
  top: calc(env(safe-area-inset-top) + 8px); left: 50%;
  display: grid; place-items: center;
  width: 34px; height: 34px;
  background: var(--bg3); border: 1px solid var(--border);
  border-radius: 50%; box-shadow: var(--shadow);
  opacity: 0; transform: translate(-50%, 0);
  pointer-events: none;
}
.ptr .spinner { animation: none; border-top-color: var(--text3); }
.ptr.armed { border-color: var(--accent-text); }
.ptr.armed .spinner { border-top-color: var(--accent-text); }
.ptr.spinning .spinner { animation: spin .8s linear infinite; border-top-color: var(--accent-text); }
.ptr.settling { transition: transform .25s ease, opacity .25s ease; }

/* ── Loading skeletons ── */
.skel {
  height: 3.6rem; border-radius: var(--radius-sm);   /* a tower bar: ~58px wide, ~80px on a phone */
  background: linear-gradient(90deg, var(--bg3) 25%, var(--bg4) 50%, var(--bg3) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.4s ease infinite;
}
.skel-row { height: 3.3rem; margin: .35rem 0; }   /* an alert row: ~65px */
/* Phone-width cards stack the score under the title (see @container card) */
@media (max-width: 480px) { .skel:not(.skel-row) { height: 5rem; } }
@keyframes shimmer { to { background-position: -200% 0; } }

/* ── Composer (fixed, thumb-reachable) ── */
.composer {
  position: fixed; z-index: 40;
  left: 0; right: 0; bottom: var(--kb);
  padding: .6rem .8rem calc(.6rem + var(--safe-b));
  /* The fade already masks what's behind; a blur over a full-width fixed
     strip re-samples the page on every scroll frame, for no visible gain. */
  background: linear-gradient(to top, var(--bg) 55%, transparent);
}
/* Keyboard is up: the home-indicator inset is not on screen anymore */
html.kb-open .composer { padding-bottom: .6rem; }

.composer-inner { position: relative; max-width: calc(680px - 2rem); margin: 0 auto; }

.track-form {
  display: flex; align-items: center; gap: .4rem;
  background: var(--bg3);
  border: 1px solid var(--border2);
  border-radius: var(--radius);
  padding: .3rem .3rem .3rem .8rem;
  transition: border-color .15s, box-shadow .15s;
}
.track-form:focus-within {
  border-color: var(--accent-text);
  box-shadow: 0 0 0 3px var(--accent-dim);
}
.track-icon { display: grid; place-items: center; flex-shrink: 0; color: var(--text3); }
.track-icon svg {
  width: 18px; height: 18px;
  fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round;
}
#trackInput {
  flex: 1; min-width: 0;
  font-family: var(--mono);
  font-size: max(16px, 1rem);      /* < 16px makes iOS zoom the page on focus */
  color: var(--text); background: none;
  border: none; outline: none;
  padding: .7rem 0; min-height: var(--tap);
}
/* The placeholder is words, set a step quieter than what you type */
#trackInput::placeholder { color: var(--text3); font-family: var(--display); }

.track-clear {
  position: relative;
  display: grid; place-items: center; flex-shrink: 0;
  width: var(--tap); height: var(--tap);   /* the disc is 32; the target isn't */
  color: var(--text3); background: none;
  border: none; border-radius: 50%; cursor: pointer;
}
.track-clear::before {
  content: ''; position: absolute; width: 32px; height: 32px;
  border-radius: 50%; background: var(--bg4);
}
.track-clear svg { position: relative; }
.track-clear svg {
  width: 14px; height: 14px;
  fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round;
}
.track-clear:active { transform: scale(.9); }

#trackBtn {
  flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
  min-height: var(--tap);
  font-family: var(--display); font-weight: 700; font-style: italic; font-size: .9rem;
  letter-spacing: .06em; text-transform: uppercase; color: var(--ink);
  background: var(--accent); border: none; border-radius: var(--radius-sm);
  padding: 0 1.1rem; cursor: pointer;
  transition: transform .1s, opacity .15s;
}
#trackBtn:active { transform: scale(.95); }
#trackBtn:disabled { opacity: .5; cursor: wait; }

.form-error {
  margin-bottom: .5rem;
  font-family: var(--display); font-size: .78rem; line-height: 1.45;
  color: var(--down); background: var(--down-dim);
  border: 1px solid var(--down); border-radius: var(--radius-sm);
  padding: .55rem .75rem;
}
/* A limit, not a failure: amber, like the "All 5 slots full" notice */
.form-error.neutral {
  color: var(--amber); background: color-mix(in srgb, var(--amber) 10%, transparent);
  border-color: color-mix(in srgb, var(--amber) 45%, transparent);
}

/* ── Search results (open upward, above the composer) ── */
.search-results {
  position: absolute; left: 0; right: 0; bottom: calc(100% + .5rem);
  z-index: 20;
  max-height: min(52dvh, 380px); overflow-y: auto;
  -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
  background: var(--bg3); border: 1px solid var(--border2);
  border-radius: var(--radius); overflow-x: hidden;
  box-shadow: var(--shadow);
}
.search-row {
  display: flex; align-items: center; gap: .7rem;
  width: 100%; min-height: var(--tap); text-align: left;
  font-family: var(--display); font-size: .9rem; line-height: 1.35;
  color: var(--text); background: none; border: none; cursor: pointer;
  padding: .75rem .9rem;
  border-bottom: 1px solid var(--border);
}
.search-row:last-child { border-bottom: none; }
/* The option Enter will take: one shade up plus an accent rail inside the
   row, so the choice is visible before you commit to it. */
.search-row[aria-selected="true"] { background: var(--bg4); box-shadow: inset 0 0 0 1px var(--accent-text); }
.search-status {
  padding: .75rem .9rem;
  font-size: .9rem; line-height: 1.45; color: var(--text2);
}
.search-row:active { background: var(--bg4); }
.search-label { flex: 1; }

/* ── Toasts ── */
.toasts {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(5.5rem + var(--safe-b) + var(--kb));
  z-index: 3100;   /* over the Settings sheet: feedback from inside it must show */
  display: flex; flex-direction: column; align-items: center; gap: .5rem;
  pointer-events: none; width: min(92vw, 480px);
}
.toast {
  font-family: var(--display); font-size: .8rem; line-height: 1.4;
  color: var(--text); background: var(--bg4);
  border: 1px solid var(--border2); border-radius: var(--radius-sm);
  padding: .7rem 1rem; max-width: 100%;
  box-shadow: var(--shadow);
  animation: toast-in .2s ease;
  transition: opacity .4s;
}
.toast.error { border-color: var(--down); color: var(--down); }
/* A toast with an action (Undo) takes taps; plain ones stay click-through */
.toast.has-action { display: flex; align-items: center; gap: .9rem; pointer-events: auto; padding-right: .3rem; }
.toast.has-action > span { flex: 1; }
.toast-action {
  flex-shrink: 0; min-height: var(--tap); padding: 0 .75rem;
  font: 700 .8rem var(--display); letter-spacing: .08em; text-transform: uppercase;
  color: var(--accent-text); background: none; border: none; border-radius: var(--radius-sm);
  cursor: pointer;
}
.toast-action:active { background: var(--accent-dim); }
.toast.gone { opacity: 0; transition-duration: .2s; }   /* leave faster than arrive */
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } }

/* ── First-run welcome ── */
/* Three short lines, not three paragraphs — anything longer got skipped
   anyway, and the app teaches the rest by being used. */
/* The welcome's one display moment: the brand face at readout size, the
   only heading on the screen that isn't a label */
.welcome-title {
  font-family: var(--brand); font-weight: 700; font-style: italic; font-size: 1.7rem;
  line-height: 1.1; letter-spacing: .01em; text-wrap: pretty;   /* balance stranded "when" on its own line */
  margin-bottom: .6rem;
}
.welcome-lede {
  margin-bottom: .2rem;
  font-size: .95rem; line-height: 1.5; color: var(--text);
}
/* The steps come after the first action, as what happens next: quieter */
.welcome .steps {
  list-style: none; counter-reset: step;
  display: flex; flex-direction: column; gap: .55rem;
  margin-top: 1.3rem;
}
.welcome .steps li {
  counter-increment: step;
  display: flex; align-items: center; gap: .7rem;
  font-size: .9rem; color: var(--text2);
}
/* Points at the search bar pinned to the bottom of the screen */
.welcome-point {
  display: flex; align-items: center; gap: .5rem;
  margin-top: 1.1rem;
  font-size: .9rem; font-weight: 600; color: var(--accent-text);
}
.welcome-point svg {
  width: 18px; height: 18px; flex-shrink: 0;
  fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round;
}
/* Numbered like tower positions: italic, no disc */
.welcome .steps li::before {
  content: counter(step);
  flex-shrink: 0; width: 1.2rem;
  font-size: .95rem; font-weight: 700; font-style: italic;
  color: var(--text3);
}
.chips-label {
  margin: 1.1rem 0 .6rem;
  font-size: .78rem; font-weight: 700; letter-spacing: .14em; text-transform: uppercase;
  color: var(--text2);
}
/* The first action: keys that fill the row rather than stragglers one per
   line with dead space beside them */
.chips { display: flex; flex-wrap: wrap; gap: .5rem; }
.chips .chip { flex: 1 1 14rem; }
.chip {
  min-height: var(--tap);
  font-size: .9rem; line-height: 1.35;
  text-align: left; color: var(--text); background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: .5rem .85rem; cursor: pointer;
  display: inline-flex; align-items: center; gap: .5rem;
  transition: border-color .15s, transform .1s, background .15s;
}
/* The source as a small lamp ahead of the name: the same colour key the
   cards spell out in words, at a size a one-tap example can carry. */
.chip::before {
  content: ''; flex-shrink: 0;
  width: 8px; height: 8px; border-radius: 2px;   /* the source's team chip */
  background: var(--text3);
}
.chip.polymarket::before { background: var(--poly); }
.chip.kalshi::before     { background: var(--kalshi); }
.chip.stock::before      { background: var(--stock); }
.chip:active { transform: scale(.96); background: var(--bg4); }
.chip:disabled { opacity: .5; cursor: wait; }

/* ── "Turn on alerts" call to action ── */
.alerts-cta {
  width: 100%; min-height: var(--tap);
  font-family: var(--display); font-weight: 700; font-style: italic; font-size: .9rem;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink); background: var(--accent);
  border: none; border-radius: var(--radius);
  padding: .8rem 1rem; cursor: pointer;
  transition: transform .1s;
}
.alerts-cta:active { transform: scale(.99); }

/* ── The tower ── */
/* One continuous stack of bars, flush to the panel's edges: the panel is
   the tower, the rows are its bars, hairlines divide them. */
.panel.tower { padding: 0; overflow: hidden; container: tower / inline-size; }
#trackedPanel > .plan-notice { margin: 0; padding: .8rem 1rem; border-top: 1px solid var(--border); }
.tower-head, .item-row {
  display: grid; align-items: center; column-gap: .7rem;
  grid-template-columns: 1.6rem minmax(0, 1fr) auto 4.4rem;
  padding-inline: .9rem;
}
/* The header row is the tower's only label: uppercase lives here and in tags */
.tower-head {
  padding-block: .55rem;
  font-size: .72rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: var(--text3);
}
.tower-head .count { font-weight: 700; color: var(--text2); margin-left: .3rem; letter-spacing: .04em; }
.tower-head h2 { font: inherit; letter-spacing: inherit; color: inherit; }
.tower-head span:nth-child(n+3) { text-align: right; }
#trackedPanel:has(.tracked-list:empty) .tower-head,
#trackedPanel:has(.skel) .tower-head { display: none; }
.tracked-list { display: flex; flex-direction: column; }
.tracked-list .skel { margin: .35rem .9rem; }

.item {
  border-top: 1px solid var(--border);
  transition: background-color .2s;
}
.item.open { background: var(--bg3); }

/* The bar: position, name, now, interval. It is the card's heading and its
   disclosure button in one. */
.item-heading { margin: 0; font: inherit; }
.item-row {
  width: 100%; min-height: var(--tap);
  padding-block: .75rem;
  font: inherit; text-align: left; color: inherit;
  background: none; border: none; cursor: pointer;
  transition: background-color .15s;
}
.item-row:focus-visible { outline-offset: -2px; }
.item-row:active { background-color: var(--accent-dim); }
/* The position cell: a block per bar, the leader's lit white. After an
   overtake a small marker sits beside it for a few seconds. */
.pos {
  position: relative;
  display: grid; place-items: center; width: 1.6rem; height: 1.6rem; box-sizing: border-box;
  border-radius: 3px; background: var(--bg4);
  font-size: .95rem; font-weight: 700; font-style: italic; line-height: 1;
  color: var(--text2);
}
.item:first-child .pos { background: var(--accent); color: var(--ink); }
/* The overtake marker: an 8px triangle inside the cell, up for a gain,
   down for a loss. The number steps left to make room, so the two never
   touch and nothing crowds the name. */
.pos::after {
  content: ''; position: absolute; right: 3px; top: 50%; margin-top: -3.5px;
  width: 8px; height: 7px;
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
  background: var(--text); opacity: 0; transition: opacity .3s;
}
.item:first-child .pos::after { background: var(--ink); }
.item.gained .pos, .item.lost .pos { padding-right: 9px; }
.item.gained .pos::after { opacity: 1; }
.item.lost .pos::after { opacity: 1; clip-path: polygon(0 0, 100% 0, 50% 100%); background: var(--text3); }
.item.lost:first-child .pos::after { background: var(--ink); }
.pos { font-variant-numeric: lining-nums; }
.item-name { min-width: 0; display: flex; flex-direction: column; gap: .2rem; }
.item-sub {
  display: flex; align-items: center; gap: .4rem; flex-wrap: wrap;
  font-size: .78rem; font-weight: 600; color: var(--text3);
}
.src-chip { width: 8px; height: 8px; border-radius: 2px; background: var(--text3); flex-shrink: 0; }
.src-chip.polymarket { background: var(--poly); }
.src-chip.kalshi     { background: var(--kalshi); }
.src-chip.stock      { background: var(--stock); }
.mv { white-space: nowrap; }
.mv::before { content: '·'; margin-right: .4rem; color: var(--text3); }
.mv.up   { color: var(--up); }
.mv.down { color: var(--down); }
.item-now { display: flex; flex-direction: column; align-items: flex-end; gap: .1rem; }
/* The interval column: yellow while far, the direction colour once close,
   a solid tag once past */
.item-gap { text-align: right; line-height: 1; }
.gap { display: inline-flex; flex-direction: column; align-items: flex-end; gap: .2rem;
  font-size: 1.4rem; font-weight: 700; line-height: 1; color: var(--gap); }
.gap small { font-size: .72rem; font-weight: 600; color: var(--text3); white-space: nowrap; }
.gap.up   { color: var(--up); }
.gap.down { color: var(--down); }
.gap.none { color: var(--text3); }
.gap-tag {
  display: inline-block; padding: .25rem .45rem; border-radius: 3px;
  font-size: .78rem; font-weight: 700; font-style: italic; letter-spacing: .04em; text-transform: uppercase;
}
.gap-tag.buzz    { background: var(--buzz); color: #ffffff; }
.gap-tag.caution { background: var(--amber); color: var(--on-amber); }
/* It just came within reach: the interval blinks twice, then holds */
.item-gap.lamp-new { animation: lamp-on .52s steps(1, end) 2; }

/* The body folds by its grid row, so it opens to its real height with no
   measuring. Hidden when shut, so nothing inside takes focus or is read. */
.item-body {
  display: grid; grid-template-rows: 0fr; visibility: hidden;
  transition: grid-template-rows .3s cubic-bezier(.16, 1, .3, 1), visibility 0s .3s;
}
.item.open .item-body {
  grid-template-rows: 1fr; visibility: visible;
  transition: grid-template-rows .3s cubic-bezier(.16, 1, .3, 1);
}
/* No vertical padding here: it would leak out of the shut 0fr row. The
   trailing spacer is content, so it folds away with the rest. */
.item-body-inner {
  min-height: 0; overflow: hidden;
  display: flex; flex-direction: column; gap: .7rem;
  padding-inline: .9rem;
}
.item-body-inner::after { content: ''; height: .2rem; }
/* Air between the meter and the first line of the body. A child's margin,
   not the box's padding, so it folds away with the rest. */
.item-body-inner > :first-child { margin-top: .35rem; }
.item.leaving { opacity: 0; transform: scale(.97); transition: opacity .2s, transform .2s; }

/* The badge only lives in search results; a tracked card spells its source
   out in its meta line. */
.badge {
  font-family: var(--mono); font-size: .72rem; font-weight: 700;
  letter-spacing: .12em; border-radius: 4px; padding: .28rem .45rem;
  text-transform: uppercase;   /* the source's full name, as the cards spell it */
  flex-shrink: 0;
}
.badge.polymarket { color: var(--poly); background: var(--poly-dim); }
.badge.kalshi     { color: var(--kalshi); background: var(--kalshi-dim); }
.badge.stock      { color: var(--stock); background: var(--stock-dim); }

.item-label {
  font-size: .95rem; line-height: 1.3; font-weight: 600;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
  color: var(--text);
}
/* Open, the whole name: there's room, and it's the one place to read it */
.item.open .item-label { -webkit-line-clamp: unset; }
/* "POLYMARKET · Following the busiest of 80 outcomes": where it comes from,
   and the choice the resolver made. The source word is the colour key. */
.item-meta { font-size: .78rem; line-height: 1.35; color: var(--text3); }
.src { font-size: .78rem; font-weight: 700; }
.src.polymarket { color: var(--poly); }
.src.kalshi     { color: var(--kalshi); }
.src.stock      { color: var(--stock); }

/* Just tracked: one flash as you land on it, like a credit going in.
   A border and fill that fade out, never a glow; reduced motion skips it. */
.item.arrived { animation: arrive 1.2s cubic-bezier(.16, 1, .3, 1) 1; }
@keyframes arrive {
  from { background-color: color-mix(in srgb, var(--text) 14%, transparent); }
}

/* One-tap alert sizes on the card you just tracked. Named for what they
   feel like, with the number underneath so the name teaches the unit. */
.presets { display: flex; flex-direction: column; gap: .5rem; margin-top: .2rem; }
.presets-label { font-size: .9rem; color: var(--text); }
/* Two by two: each name fits on one line even on a 320px screen */
.preset-row { display: grid; grid-template-columns: repeat(2, 1fr); gap: .4rem; }
/* The three sizes are one radiogroup; Custom is a plain key beside them.
   The group steps out of the box so all four share the one grid. */
.preset-radios { display: contents; }
.preset {
  min-height: var(--tap);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .15rem;
  font-family: var(--display); font-weight: 700; font-style: italic; font-size: .78rem;
  letter-spacing: .04em; text-transform: uppercase; text-align: center;
  color: var(--text); background: var(--bg4);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: .45rem .3rem; cursor: pointer;
  transition: transform .1s, background .15s, border-color .15s;
}
.preset-size {
  font-size: .82rem; letter-spacing: 0; text-transform: none;
  color: var(--gap);
}
.preset:active { transform: scale(.96); background: var(--accent-dim); }
.preset[aria-checked="true"] { border-color: var(--accent-text); background: var(--accent-dim); }
/* Custom: the same key, one step quieter, so the three named sizes lead */
.preset-custom { background: none; border-style: dashed; }
.preset-custom .preset-size { color: var(--text2); }

/* Step 3, in the card you just set up */
.card-alerts {
  width: 100%; min-height: var(--tap);
  font: italic 700 .82rem var(--display); letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink); background: var(--accent);
  border: none; border-radius: var(--radius-sm); cursor: pointer;
  transition: transform .1s;
}
.card-alerts:active { transform: scale(.98); }
/* "64%" alone never says 64% OF WHAT, and this app is aimed at people with
   no markets background. One quiet word answers it. Prices don't need it. */
.value-unit { font-size: .72rem; font-weight: 600; color: var(--text3); }
.item-value {
  font-size: 1.4rem; font-weight: 700; line-height: 1;
  white-space: nowrap;
}
.item-value .dec { font-size: .78em; }

/* ── Chart: the trend inside its alert band ──
   Chart and caption are one group (tight), set apart from the controls
   below (generous), so the card reads: what it is → where it's heading →
   what you can change. The band's recessed slot divides the card, so the
   footer no longer needs a rule of its own. */
.item-chart { display: flex; flex-direction: column; gap: .35rem; }

.spark { display: block; width: 100%; height: 44px; }
/* The slot between the two trigger levels: one shade down, like a well */
.spark .band { fill: var(--bg4); }
.spark .trip { stroke: var(--text3); stroke-width: 1; }
.spark polyline {
  fill: none; stroke: var(--text2); stroke-width: 2;
  stroke-linejoin: round; stroke-linecap: round;
}
/* Near a trigger: the edge being approached switches on. Direction is data,
   so it takes the up/down colour, never the accent, and never a glow. */
.spark .zone.up   { fill: var(--up-dim); }
.spark .zone.down { fill: var(--down-dim); }
.spark .trip.lamp { stroke-width: 1.5; }
.spark .trip.lamp.up   { stroke: var(--up); }
.spark .trip.lamp.down { stroke: var(--down); }
/* It just came within reach: the lit edge and zone blink twice like a
   marquee bulb catching, then hold. Hard steps, not a fade: a switch, not
   a glow. Runs once, only when the state changes between refreshes. */
.spark.lamp-new .zone,
.spark.lamp-new .trip.lamp { animation: lamp-on .52s steps(1, end) 2; }
@keyframes lamp-on { 0% { opacity: 0; } 50% { opacity: 1; } }
/* The live point: where it is now */
.spark .dot {
  fill: none; stroke: var(--text); stroke-width: 5; stroke-linecap: round;
}

/* The chart's caption: the alert size turned into the two values that will
   actually buzz you. The values themselves in bold. */
.trigger {
  font-size: .8rem; line-height: 1.4; color: var(--text2);
  text-wrap: balance;
}
.trigger .lvl {
  font-weight: 700; color: var(--text);
  white-space: nowrap;
}
/* Within reach of its alert: said in words, in the direction colour */
.trigger .near { font-weight: 600; }
.trigger .near.up   { color: var(--up); }
.trigger .near.down { color: var(--down); }
.trigger .near.caution { color: var(--amber); }   /* past, but it can't reach you */

/* "Alert at [− 3 pts +]  ×" on one row. It never wraps: a wrap used to
   orphan the trash key on a line of its own. On the narrowest screens the
   label folds onto two lines instead. */
.item-foot {
  display: flex; align-items: center; gap: .5rem;
  margin-top: .2rem;   /* .7rem card gap + this = the generous break */
}
.foot-label { min-width: 0; font-size: .8rem; line-height: 1.25; color: var(--text2); }

/* Stepper — tapping ± beats typing into a tiny number field on a phone.
   The box was sized for a longer readout than "3 pts" ever is, so it read
   as a big empty control with a small number rattling around inside it.
   Trimmed the air and grew the number until they're in proportion. */
.stepper {
  flex-shrink: 0;   /* the label wraps; the keys never get squeezed out */
  display: flex; align-items: center;
  background: var(--bg4); border: 1px solid var(--border);
  border-radius: var(--radius-sm); overflow: hidden;
}
.step {
  position: relative; flex: 0 0 var(--tap);
  width: var(--tap); height: var(--tap);
  display: grid; place-items: center;
  font-family: var(--mono); font-size: 1.1rem; line-height: 1;
  color: var(--text2); background: none; border: none; cursor: pointer;
  transition: color .15s, background .15s;
}
.step:active { background: var(--accent-dim); color: var(--accent-text); }
.step-field {
  display: flex; align-items: center; gap: .12rem; align-self: stretch;   /* tap anywhere in the row */
  font-family: var(--mono); color: var(--text);
  justify-content: center;
}
/* Pinned to 4ch — room for the longest clamped value ("12.5"). Sizing it to
   the current value packed the box tighter, but the width then changed under
   your thumb as you stepped past a decimal, which was worse in the hand than
   the spare character of air is on the page. */
.step-field input {
  width: 4ch; text-align: right;
  font-family: var(--mono); font-size: max(17px, 1.06rem); font-weight: 700;
  color: var(--accent-text); background: none;
  border: none; outline: none; padding: 0;
  -moz-appearance: textfield;
}
.stepper:focus-within:has(input:focus-visible),
.stepper:has(input:focus) { box-shadow: 0 0 0 2px var(--accent-text); }
.step-field input::-webkit-outer-spin-button,
.step-field input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.step-unit { font-size: .78rem; color: var(--text2); }

.del-btn {
  position: relative;
  margin-left: auto; flex-shrink: 0;
  display: grid; place-items: center;
  width: var(--tap); height: var(--tap);   /* matches the stepper's height */
  color: var(--text3); background: none;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  cursor: pointer;
  transition: color .15s, border-color .15s, background .15s;
}

.del-btn svg {
  width: 17px; height: 17px;
  fill: none; stroke: currentColor; stroke-width: 1.9;
  stroke-linecap: round; stroke-linejoin: round;
}
.del-btn:active { background: var(--bg4); }

/* ── Buzz log: a second tower ── */
/* One bar per buzz, flush to the log's edges: flag · name · move · age.
   The size of the move is the score, in the direction colour; from → to
   backs it up a step quieter. A bar whose market you track is a button to
   its row in the tower. */
.alert-list { display: flex; flex-direction: column; }
.alert-row { border-top: 1px solid var(--border); }
.alert-inner {
  display: grid; align-items: center; column-gap: .6rem; row-gap: .3rem;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  grid-template-areas: "tag name move time";
  width: 100%; padding: .75rem .9rem;
  font: inherit; text-align: left; color: inherit;
  background: none; border: none;
}
.alert-head { display: contents; }
button.alert-inner { cursor: pointer; transition: background-color .15s; }
button.alert-inner:active { background-color: var(--bg3); }
button.alert-inner:focus-visible { outline-offset: -2px; }
.log .empty { padding: .9rem; border-top: 1px solid var(--border); }
/* Race control's flag: purple for a buzz you haven't seen, a dim tag once seen */
.buzz-tag {
  grid-area: tag;
  padding: .15rem .4rem; border-radius: 3px;
  font-size: .72rem; font-weight: 700; font-style: italic; letter-spacing: .04em; text-transform: uppercase;
  color: var(--text2); background: var(--bg4);
}
.alert-row.fresh .buzz-tag { color: #ffffff; background: var(--buzz); }
.alert-label {
  grid-area: name; min-width: 0;
  font-size: .9rem; line-height: 1.35; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.alert-time {
  grid-area: time; justify-self: end;
  display: inline-flex; align-items: center; gap: .35rem;
  font-size: .78rem; font-weight: 600; color: var(--text3); white-space: nowrap;
}
.alert-move {
  grid-area: move;
  display: flex; align-items: baseline; gap: .5rem; white-space: nowrap;
  font-size: .95rem; font-weight: 700;
}
.alert-move.up   { color: var(--up); }
.alert-move.down { color: var(--down); }
.alert-dir { width: .72rem; height: .72rem; flex-shrink: 0; fill: currentColor; align-self: center; }
.alert-span { font-size: .8rem; font-weight: 400; color: var(--text2); }
/* New since your last visit: the buzzes you got while away. The name comes
   up to full weight; seen bars stay a step down. */
.alert-row.fresh .alert-label { font-weight: 700; }
.alert-row.fresh .alert-time { color: var(--text); }
.fresh-dot { display: none; }   /* the purple tag says it */
.alert-row:not(.fresh) .alert-label { color: var(--text2); }
/* Narrow logs: name and age on the flag's line, the move under the name.
   One line kept "Will the Fed cut rates in Dece…", the loss the tower
   already refuses, so the name gets two. */
@container tower (max-width: 30em) {
  .alert-inner { grid-template-columns: auto minmax(0, 1fr) auto; grid-template-areas: "tag name time" ". move move"; align-items: start; }
  .alert-label { white-space: normal; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
  .buzz-tag, .alert-time { margin-top: .1rem; }
}

/* Alerts refused: something to fix, not an error — amber, like a stalled
   check, with the words saying where the switch is */
.banner.blocked { border-color: color-mix(in srgb, var(--amber) 45%, transparent); }
.banner.blocked strong { color: var(--amber); }

/* ── Before the permission prompt ──
   A bottom sheet on a phone, a card in the middle on a wide screen. The
   mock is drawn like the phone's own notification, not like Pulse: it's a
   preview of something that will arrive outside the app. */
.primer {
  position: fixed; inset: auto 0 0; margin: 0 auto;
  width: 100%; max-width: 480px; max-height: none;
  padding: 0;
  color: var(--text); background: var(--sheet-bg);
  border: 1px solid var(--border2); border-bottom: none;
  border-radius: 20px 20px 0 0;
  box-shadow: var(--shadow-lift);
}
.primer[open] { display: block; animation: primer-up .32s cubic-bezier(.16, 1, .3, 1); }
/* All the padding is on the inner box, so a tap that lands on the dialog
   element itself can only be the backdrop around it */
.primer-inner {
  display: flex; flex-direction: column; gap: 1rem;
  padding: 1.4rem 1rem calc(1.2rem + var(--safe-b));
}
.primer::backdrop { background: var(--scrim); }
@keyframes primer-up { from { transform: translateY(24px); opacity: 0; } }
.primer h2 {
  font-family: var(--brand); font-size: 1.1rem; font-weight: 700; font-style: italic;
  letter-spacing: .02em; text-transform: uppercase;
}
.notif-mock {
  display: flex; gap: .75rem; align-items: flex-start;
  padding: .8rem .9rem;
  background: var(--bg3); border: 1px solid var(--border);
  border-radius: var(--radius);
}
.notif-icon {
  flex-shrink: 0; display: grid; place-items: center;
  width: 36px; height: 36px; border-radius: var(--radius-sm);
  background: var(--icon-tile);
}
.notif-icon svg {
  width: 22px; height: 22px; fill: none; stroke: var(--icon-mark); stroke-width: 2.1;
  stroke-linecap: round; stroke-linejoin: round;
}
.notif-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: .2rem; }
.notif-head {
  display: flex; justify-content: space-between; gap: .5rem;
  font-family: system-ui, sans-serif; font-size: .9rem; font-weight: 600;
}
.notif-time { font-weight: 400; font-size: .78rem; color: var(--text3); }
.notif-body { font-family: system-ui, sans-serif; font-size: .9rem; line-height: 1.4; color: var(--text2); }
.primer-note { font-size: .9rem; line-height: 1.5; color: var(--text2); }
.primer-actions { display: flex; gap: .6rem; }
.primer-actions button {
  flex: 1; min-height: var(--tap);
  font-family: var(--display); font-weight: 700; font-style: italic; font-size: .9rem;
  letter-spacing: .08em; text-transform: uppercase;
  border-radius: var(--radius-sm); cursor: pointer;
  transition: transform .1s;
}
.primer-yes { color: var(--ink); background: var(--accent); border: none; }
.primer-no  { color: var(--text2); background: var(--bg3); border: 1px solid var(--border); }
.primer-actions button:active { transform: scale(.97); }
@media (min-width: 720px) {
  .primer { inset: 0; margin: auto; height: fit-content; border-radius: 20px; border-bottom: 1px solid var(--border2); }
  .primer-inner { padding-bottom: 1.2rem; }
}

/* ── The formation lap ──
   A spotlight cut from the dimmed page (its giant shadow is the dimming)
   travels from bar to readout to the CLOSE figure; the caption is a
   broadcast lower-third that wipes on at each step. */
.tour { position: fixed; inset: 0; z-index: 3200; opacity: 0; transition: opacity .2s ease; }
.tour.on { opacity: 1; }
.tour-spot {
  position: fixed; left: 0; top: 0; width: 0; height: 0;
  border-radius: var(--radius-sm);
  box-shadow: 0 0 0 200vmax var(--scrim);
  outline: 2px solid var(--text); outline-offset: 0;
  transition: transform .42s cubic-bezier(.16, 1, .3, 1), width .42s cubic-bezier(.16, 1, .3, 1), height .42s cubic-bezier(.16, 1, .3, 1);
  pointer-events: none;
}
.tour-card {
  position: fixed; left: .8rem; right: .8rem; bottom: calc(5.6rem + var(--safe-b));
  max-width: 520px; margin: 0 auto;
  background: var(--bg2); color: var(--text);
  border: 1px solid var(--border2); border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: .9rem 1rem .8rem;
}
/* Lower-third wipe: the card is revealed left to right, the way a
   broadcast graphic comes on */
.tour-card.wipe { animation: tour-wipe .34s cubic-bezier(.16, 1, .3, 1); }
@keyframes tour-wipe { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
.tour-step {
  font-size: .72rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: var(--text3);
}
.tour-title {
  margin-top: .2rem;
  font-size: 1.1rem; font-weight: 700; font-style: italic; letter-spacing: .01em;
}
.tour-text { margin-top: .35rem; font-size: .95rem; line-height: 1.45; color: var(--text2); }
.tour-actions { display: flex; justify-content: flex-end; gap: .5rem; margin-top: .8rem; }
.tour-actions button {
  min-height: var(--tap); padding: 0 1.1rem; border-radius: var(--radius-sm);
  font: italic 700 .9rem var(--display); letter-spacing: .06em; text-transform: uppercase;
  cursor: pointer;
}
.tour-next { color: var(--ink); background: var(--accent); border: none; }
.tour-skip { color: var(--text2); background: none; border: 1px solid var(--border2); }
.tour-next:active, .tour-skip:active { transform: scale(.97); }
/* The demo's own label, so nobody mistakes it for a real buzz */
.tour-eg {
  display: block; margin-top: .3rem;
  font-size: .72rem; font-weight: 600; color: var(--text3); text-align: right;
}
body.touring { overflow: hidden; }

/* ── Bottom sheet ── */
.sheet-backdrop {
  position: fixed; inset: 0; z-index: 2900;
  background: var(--scrim);
  opacity: 0; transition: opacity .28s ease;
}
.sheet-backdrop.open { opacity: 1; }

.sheet {
  position: fixed; z-index: 3000;
  left: 0; right: 0; bottom: 0;
  max-width: 680px; margin: 0 auto;
  max-height: 88dvh;
  display: flex; flex-direction: column;
  background: var(--sheet-bg);
  border: 1px solid var(--border2); border-bottom: none;
  border-radius: 20px 20px 0 0;
  box-shadow: var(--shadow-lift);
  transform: translateY(100%);
  transition: transform .3s cubic-bezier(.32, .72, 0, 1);
}
.sheet.open { transform: translateY(0); }
.sheet.dragging { transition: none; }

.sheet-grab {
  display: grid; place-items: center;
  padding: .7rem 0 .35rem;
  cursor: grab; touch-action: none;
}
.sheet-grab span {
  display: block; width: 40px; height: 4px;
  background: var(--text3); border-radius: 2px; opacity: .6;
}
.sheet-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 1rem .5rem;
}
.sheet-head h2 {
  font-family: var(--brand); font-size: 1.1rem; font-weight: 700; font-style: italic;
  letter-spacing: .02em; text-transform: uppercase;
}
.sheet-body {
  flex: 1; overflow-y: auto;
  -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
  padding: .5rem 1rem calc(1.5rem + var(--safe-b));
  display: flex; flex-direction: column; gap: 1.4rem;
}

.sheet-label {
  font-size: .72rem; font-weight: 700; letter-spacing: .12em;
  text-transform: uppercase; color: var(--text3);
  margin-bottom: .55rem;
}

/* Settings rows */
.row {
  display: flex; align-items: center; gap: .8rem;
  width: 100%; min-height: var(--tap); text-align: left;
  background: var(--bg3); border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: .8rem .9rem;
}
.row-btn { cursor: pointer; transition: background .15s, transform .1s; }
.row-btn:active { background: var(--bg4); transform: scale(.99); }
.row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: .15rem; }
.row-title { font-size: .95rem; font-weight: 600; color: var(--text); }
.row-sub { font-size: .78rem; line-height: 1.4; color: var(--text2); }
.row-chevron { font-size: 1.1rem; color: var(--text3); line-height: 1; }
.row-note {
  font-size: .82rem; line-height: 1.5; color: var(--text2);
  margin-bottom: .7rem;
}

/* Switch */
.switch {
  flex-shrink: 0; position: relative;
  width: 50px; height: 30px;
  background: var(--bg4); border: 1px solid var(--border);
  border-radius: 999px; transition: background .2s, border-color .2s, box-shadow .2s;
}
.switch .knob {
  position: absolute; top: 3px; left: 3px;
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--text3);
  transition: transform .2s cubic-bezier(.32, .72, 0, 1), background .2s;
}
[aria-pressed="true"] .switch {
  background: var(--accent); border-color: var(--accent);
}
[aria-pressed="true"] .switch .knob { transform: translateX(20px); background: var(--ink); }

/* Segmented control */
.segmented {
  display: flex; gap: .25rem;
  background: var(--bg3); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: .25rem;
}
.seg {
  flex: 1; min-height: var(--tap);
  font-family: var(--display); font-size: .82rem; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--text2); background: none;
  border: none; border-radius: 6px; cursor: pointer;
  transition: background .15s, color .15s;
}
.seg[aria-pressed="true"] {
  color: var(--ink); background: var(--accent);
}

/* ── Account ── */
.auth-form { display: flex; flex-direction: column; gap: .6rem; }
.auth-form input {
  min-height: var(--tap);
  font-family: var(--mono);
  font-size: max(16px, 1rem);      /* keep iOS from zooming on focus */
  color: var(--text); background: var(--bg3);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: .75rem .9rem; outline: none;
}
.auth-form input:focus { border-color: var(--accent-text); }
.auth-form input:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 1px; }
.auth-form input::placeholder { color: var(--text3); }
.auth-actions { display: flex; gap: .6rem; margin-top: .2rem; }
.auth-actions button {
  flex: 1; min-height: var(--tap);
  font-family: var(--display); font-weight: 700; font-style: italic; font-size: .9rem;
  letter-spacing: .08em; text-transform: uppercase;
  border-radius: var(--radius-sm); padding: .7rem 0; cursor: pointer;
  color: var(--ink); background: var(--accent); border: none;
  transition: transform .1s;
}
.auth-actions button:active { transform: scale(.97); }
.auth-actions button.secondary {
  color: var(--text2); background: var(--bg3);
  border: 1px solid var(--border);
}
.auth-actions button:disabled { opacity: .5; cursor: wait; }
/* A quiet way out, not a third button: underlined words, full-size target */
.text-btn {
  align-self: flex-start; min-height: var(--tap); padding: 0 .1rem;
  font: 600 .82rem var(--display); color: var(--accent-text);
  background: none; border: none; cursor: pointer;
  text-decoration: underline; text-decoration-color: var(--border2); text-underline-offset: 4px;
}
.text-btn:disabled { opacity: .5; cursor: wait; }
.auth-info {
  margin-top: .7rem; font-size: .9rem; line-height: 1.5; color: var(--text2);
}
.ghost-btn {
  flex-shrink: 0; min-height: var(--tap);
  font-family: var(--display); font-size: .72rem;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--text2); background: var(--bg4);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 0 .85rem; cursor: pointer;
}
.ghost-btn:active { color: var(--down); border-color: var(--down); }

/* Destructive, and deliberately not styled like the accent CTAs */
.danger-note { margin: .9rem 0 .6rem; }
.danger-btn {
  width: 100%; min-height: var(--tap);
  font-family: var(--display); font-size: .78rem;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--down); background: none;
  border: 1px solid var(--down); border-radius: var(--radius-sm);
  padding: 0 .85rem; cursor: pointer;
  transition: background .15s, transform .1s;
}
.danger-btn:active { background: var(--down-dim); transform: scale(.99); }
.danger-btn.confirm { color: var(--ink-danger); background: var(--down); border-color: var(--down); }
.danger-btn:disabled { opacity: .5; cursor: wait; }

/* ── Misc ── */
.empty { padding: .6rem 0; text-align: center; }
.empty-sub { font-size: .9rem; color: var(--text3); }
.mono { font-family: var(--mono); color: var(--text); }

.foot {
  font-family: var(--display); font-size: .72rem; line-height: 1.5;
  color: var(--text3); text-align: center;
}

/* Privacy / Support, at the bottom of the settings sheet. Store requirement,
   so it has to be findable — but it isn't what anyone opened settings for. */
.legal-links {
  display: flex; justify-content: center; gap: .5rem;
  font-size: .8rem;
}
/* Small type, full-size target: the link is 44px tall even though the words
   stay quiet. Underline instead of a border so it hugs the text, not the box. */
.legal-links a {
  display: inline-flex; align-items: center;
  min-height: var(--tap); padding: 0 .75rem;
  color: var(--text2);
  text-decoration: underline; text-decoration-color: var(--border2);
  text-underline-offset: 4px;
}

/* ── Large text (the phone's text size setting) ──
   Cards and the composer are size containers measured in em, so these kick
   in when the text grows, not when the screen shrinks: at normal size a
   360px phone card is ~16em wide; at 150% it drops below 14em. */
/* Arrow form of Track, for when the word no longer fits (below) */
#trackBtn .track-go {
  display: none; width: 1.1em; height: 1.1em;
  fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round;
}
.item { container: card / inline-size; }
.composer-inner { container: composer / inline-size; }
/* Phone-width cards: the score column left the title about 140px, and a
   title cut to "Highest temperature in NYC today abov…" loses the part that
   matters. So the title takes the full width and the score line sits under
   it, value on the left under the title, move on the right. Still a glance:
   two short lines instead of one squeezed one. */
/* Phone-width towers keep every column; the columns tighten and the
   readouts step down one size. A long title may take a third line rather
   than lose its condition ("…above 80°F?"). */
@container tower (max-width: 26em) {
  .tower-head, .item-row { grid-template-columns: 1.6rem minmax(0, 1fr) auto 3.6rem; column-gap: .55rem; padding-inline: .75rem; }
  .item-value, .gap { font-size: 1.3rem; }   /* still the loudest thing on the bar */
  .item-label { -webkit-line-clamp: 3; }
}
@container card (max-width: 14em) {
  .item-foot { flex-wrap: wrap; }
  .foot-label { flex-basis: 100%; }
  .stepper { flex: 1 1 auto; justify-content: space-between; }
  .item-label { -webkit-line-clamp: 4; }
}
@container composer (max-width: 17em) {
  .track-icon { display: none; }
  #trackBtn { padding: 0 .75rem; }
  #trackBtn .track-text { display: none; }
  #trackBtn .track-go { display: block; }
}

/* ── Android: Material conventions inside the same world ──
   48dp targets, a start-aligned app bar title, a Material switch and a
   snackbar. Set from the runtime platform, never from screen size. */
.platform-android { --tap: 48px; }
.platform-android .topbar { grid-template-columns: auto 1fr auto; }
.platform-android .topbar-side { display: none; }
.platform-android .brand { justify-self: start; grid-column: 1; }
.platform-android .topbar-actions { grid-column: 3; }
.platform-android .switch { width: 52px; height: 32px; }
/* Material thumb: small when off, full when on. Scaled, not resized, so the
   change stays on the compositor. */
.platform-android .switch .knob { top: 3px; left: 3px; width: 24px; height: 24px; transform: scale(.67);
  transition: transform .2s cubic-bezier(.2, 0, 0, 1), background .2s; }
.platform-android [aria-pressed="true"] .switch .knob { transform: translateX(20px) scale(1); }
.platform-android .toasts { left: .75rem; right: .75rem; width: auto; transform: none; align-items: stretch; }
.platform-android .toast { border-radius: 4px; }

/* ── Wider screens: hover affordances and a roomier grid ── */
@media (min-width: 720px) {
  .shell { padding-top: 1.5rem; gap: 1.2rem; }
  .sheet {
    border-radius: 20px; bottom: 1.5rem; max-width: 480px;
    border-bottom: 1px solid var(--border2);
    transform: translateY(calc(100% + 2rem));
  }
  .sheet.open { transform: translateY(0); }
}

@media (hover: hover) and (pointer: fine) {
  .icon-btn:hover { color: var(--text); background: var(--bg3); }
  .icon-btn.on:hover { color: var(--accent-text); }
  .search-row:hover { background: var(--bg4); }
  .chip:hover { border-color: var(--border2); background: var(--bg4); }
  .del-btn:hover { color: var(--down); border-color: var(--down); }
  .step:hover { color: var(--accent-text); }
  .item-row:hover { background-color: color-mix(in srgb, var(--text) 4%, transparent); }
  button.alert-inner:hover { background-color: var(--bg3); }
  .preset:hover { border-color: var(--border2); }
  .row-btn:hover { background: var(--bg4); }
  .danger-btn:hover { background: var(--down-dim); }
  .seg:not([aria-pressed="true"]):hover { color: var(--text); }
}

/* Reduced motion means fewer, gentler movements, not none. Anything that
   travels, scales or blinks stops; changes of colour and opacity stay, kept
   short, so every press, arrival, lamp and sent buzz still registers. */
@media (prefers-reduced-motion: reduce) {
  :active { transform: none !important; }
  .toast { animation: toast-fade .15s ease; }
  .item.leaving { transform: none; }
  .skel { animation: none; }
  .spinner { animation-duration: 1.6s; }
  /* The sheet appears in place instead of rising */
  .sheet { transform: none; opacity: 0; transition: opacity .2s ease; }
  .sheet.open { opacity: 1; }
  .ptr.settling { transition: opacity .2s ease; }
  /* The credit: the logo brightens instead of drawing itself */
  .brand-mark.credit path { animation: credit-fade .4s ease 1; }
  /* The lamp comes on with a short fade instead of blinking */
  .spark.lamp-new .zone,
  .spark.lamp-new .trip.lamp { animation: lamp-fade .3s ease 1; }
  .item-gap.lamp-new { animation: lamp-fade .3s ease 1; }
  /* A card opens and folds in place instead of sliding */
  .item-body, .item.open .item-body { transition: none; }
  .primer[open] { animation: toast-fade .15s ease; }
  /* The lap jumps between targets and the card appears in place */
  .tour-spot { transition: none; }
  .tour-card.wipe { animation: toast-fade .15s ease; }
}
@keyframes toast-fade  { from { opacity: 0; } }
@keyframes credit-fade { from { opacity: .25; } }
@keyframes lamp-fade   { from { opacity: 0; } }
