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

/* ═══════════════════════════════════════════════════════════════════════
   TickWise design tokens

   Thesis: an instrument panel where colour is a vocabulary, not paint.
   Three registers, never mixed: the chrome is a neutral cool-cast ramp;
   INTERACTION — the primary action, the active nav, focus — speaks in a
   single brand blue; and DATA keeps sole ownership of the three semantic
   hues (win / loss / pending). Blue sits equidistant from all three on the
   wheel, so "you can click this" and "this is a P&L outcome" stay
   distinguishable at a glance.

   This app was achromatic until now, and the earlier thesis said so: colour
   was spent entirely on outcomes, leaving white-at-varying-alpha to express
   every interaction. That was right for an operator's dashboard where every
   viewer already knew the controls. It stops being right for a product with
   a paywall and an upgrade path, because a call to action then competes with
   the data for the same greys. The estate's other app had already resolved
   this the other way, so the blue below is adopted rather than invented —
   one language, and the app that changes is this one.

   MIGRATION IS INCREMENTAL AND HAS NOT STARTED. These tokens are declared
   here and used by nothing yet, so this commit renders byte-identically;
   `--accent` had zero call sites in the repo when it was repointed. Each
   surface moves to the blue deliberately, with a screenshot, not by a
   find-and-replace.
   ═══════════════════════════════════════════════════════════════════════ */
:root {
  /* ── Chrome: a cool-cast neutral ramp ─────────────────────────────
     Shared with the hub, byte for byte, and must move as a set or not at
     all. --text-3 in particular is read by signals.js for chart axis text
     with a matching fallback literal, which is what makes these values the
     estate's source of truth rather than the hub's. */
  --bg:         #06070A;
  --surface:    #0C0E13;   /* header, cards, table */
  --surface-2:  #12151C;   /* popovers, inputs, hover */
  --surface-3:  #191D26;   /* selected / pressed */
  --line:       #1A1E27;   /* hairline dividers */
  --line-2:     #262C38;   /* borders that need weight */
  --text:       #F0F3F8;
  --text-2:     #97A2B4;
  /* Retuned #5A6473 -> #76808F (same cool cast, +28/channel): the old
     value measured 3.05-3.36:1 on these surfaces - under WCAG AA's 4.5:1
     at every size this app sets it. Now 4.83 on --surface, 4.57 on
     --surface-2, 5.04 on --bg. Known accepted miss: 4.22 on --surface-3
     (badge.na only). Kept as opaque hex: signals.js reads it for chart
     axis text and carries a matching fallback literal. */
  --text-3:     #76808F;

  /* ── Semantics: the only saturated values in the system ─────────────
     Teal-green and rose-red rather than stock green/red: they differ in
     hue AND value, so they stay separable under deuteranopia — which for
     a P&L readout is a requirement, not a nicety. Both hold chroma on
     near-black without the vibration pure red/green produce. */
  --win:       #2FE3A0;
  --win-dim:   rgba(47,227,160,.13);
  --win-line:  rgba(47,227,160,.34);
  --loss:      #FF4D6D;
  --loss-dim:  rgba(255,77,109,.13);
  --loss-line: rgba(255,77,109,.34);
  --live:      #FFC246;
  --live-dim:  rgba(255,194,70,.13);
  --live-line: rgba(255,194,70,.34);

  /* ── Brand: one accent for interaction ──────────────────────────────
     Electric blue at hue ~222° — equidistant from win (teal 160°), loss
     (rose 349°) and live (amber 40°), so interactive ≠ data at a glance.
     Measured on this app's ramp: 6.10:1 on --surface and 6.37:1 on --bg,
     both as text on dark and as a fill under near-black text, so the same
     hex works in either role.
     Never spend it on a STATUS fact. A tinted tile saying "this account is
     an admin" is not something you can click, and using the one colour
     reserved for "you can" to say "this is" is how the vocabulary rots. */
  --brand:        #5B8CFF;
  --brand-bright: #7DA4FF;               /* hover */
  --brand-dim:    rgba(91,140,255,.13);  /* tint fills */
  --brand-line:   rgba(91,140,255,.34);  /* tinted borders */
  --brand-glow:   rgba(91,140,255,.07);  /* page-background radial */

  /* ── Type: two families. The number is the headline, so the mono face
     is the protagonist and carries every figure in the app. ─────────── */
  --font-ui:   'Archivo', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', monospace;
  --fs-micro:  10px;   /* tracked uppercase labels */
  --fs-xs:     11px;
  --fs-sm:     12.5px; /* default UI */
  --fs-md:     14px;
  --fs-lg:     20px;   /* page title */
  --fs-num:    22px;   /* readout values */

  /* ── Geometry. Spacing values are unchanged from the previous system
     so page rhythm stays stable; only the radii tighten. ────────────── */
  --sp-1: 8px; --sp-2: 12px; --sp-3: 16px; --sp-4: 24px; --sp-5: 32px;
  --sp-0: 4px;
  --r-sm: 6px; --r-md: 9px; --r-lg: 12px; --r-pill: 999px;

  --shadow-1: 0 1px 2px rgba(0,0,0,.5);
  --shadow-2: 0 18px 44px -28px rgba(0,0,0,.9);
  /* The inset top-light is what makes a surface read as elevated on a
     near-black page; the two drop shadows alone just darken the gap. It is
     also the channel tier is expressed in — Basic sits flat on --surface,
     Pro sits raised — because elevation is a material property and every
     hue in this system is already spoken for. */
  --shadow-card: 0 1px 2px rgba(0,0,0,.5), 0 8px 24px -16px rgba(0,0,0,.7),
                 inset 0 1px 0 rgba(255,255,255,.03);
  --t: 120ms ease;

  /* ── Legacy aliases ─────────────────────────────────────────────────
     Older selectors (chiefly in signals.html) still reference these
     names. Mapping rather than deleting means anything not yet migrated
     renders in the new palette instead of falling back to an unset
     custom property. */
  --surface-alt: var(--surface-2);
  --border:      var(--line);
  --border-2:    var(--line-2);
  --muted:       var(--text-3);
  /* Repointed from --text-2 to the brand blue. Zero call sites in this repo
     when it changed, so this is a rename of a dead name to the right target
     rather than a repaint — verified, not assumed. */
  --accent:      var(--brand);
  --accent-2:    var(--brand-bright);
  --green:       var(--win);
  --red:         var(--loss);
  --yellow:      var(--live);
  --radius-sm:   var(--r-sm);
  --radius-md:   var(--r-md);
  --font-body:   var(--font-ui);
  --font-display:var(--font-ui);
  --shadow:      var(--shadow-2);
}

/* ── Base ─────────────────────────────────────────────────────────── */
html { background: var(--bg); }
body {
  background: var(--bg); color: var(--text);
  font-family: var(--font-ui); font-size: var(--fs-md); line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
/* One focus treatment for the whole app, matching the hub's exactly: a brand
   ring. 6.10:1 against --surface and 6.37:1 against --bg, both far above the
   3:1 WCAG 2.2 asks of a focus indicator.
   THE OFFSET IS LOAD-BEARING, not spacing. It puts the ring on the page
   background rather than on the control, which is what keeps it legible
   around a brand-FILLED primary button — ring and fill are the same hex, so
   without the 2px gap of background between them the indicator would vanish
   into the button it is marking. Do not remove it. */
:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
a { color: inherit; }
/* The UA's `[hidden] { display: none }` is user-agent origin, so ANY author
   display value beats it — a panel declaring `display: grid` stays visible
   after `el.hidden = true`, silently. Toggled views are common enough here
   to fix once, at the source. */
[hidden] { display: none !important; }

/* ── Header ───────────────────────────────────────────────────────────
   Scoped to `body > header` so panel/card <header> elements inside page
   content never inherit the top-bar treatment. */
body > header {
  display: flex; align-items: center; gap: var(--sp-3);
  height: 48px; padding: 0 var(--sp-4); flex-shrink: 0;
  padding-left:  max(var(--sp-4), env(safe-area-inset-left));
  padding-right: max(var(--sp-4), env(safe-area-inset-right));
  background: var(--surface); border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 50;
}
body > header .logo {
  font-size: 15px; letter-spacing: -.02em; white-space: nowrap;
  text-decoration: none; color: inherit;
}
body > header .logo .tick { font-weight: 700; color: var(--text); }
body > header .logo .wise { font-weight: 500; color: var(--text-2); }
body > header nav { display: flex; gap: var(--sp-0); margin-left: var(--sp-1); }
body > header nav a {
  color: var(--text-3); text-decoration: none;
  padding: 5px 11px; border-radius: var(--r-sm);
  font-size: var(--fs-sm); font-weight: 600; line-height: 1.2;
  transition: color var(--t), background var(--t);
}
body > header nav a:hover  { color: var(--text-2); background: var(--surface-2); }
/* Active keeps --text rather than taking --brand as its colour, which is the
   whole trick: brand and --text-3 sit at almost the same luminance (1.26:1),
   so recolouring the label would have swapped a strong value contrast for a
   hue-only one and made "where am I" harder to scan, not easier. The white
   stays; the brand arrives as a tint and a ring, so the cue is value AND hue
   AND a border — never colour alone. */
body > header nav a.active {
  color: var(--text); background: var(--brand-dim);
  box-shadow: inset 0 0 0 1px var(--brand-line);
}
/* Anonymous marketing header: the same nav pills, pushed to the right. */
body > header nav.marketing-nav { margin-left: auto; align-items: center; }

.topbar-user {
  margin-left: auto; display: flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-sm); color: var(--text-3); white-space: nowrap;
}
.topbar-user .who { font-weight: 500; }
.topbar-user .role {
  font-family: var(--font-mono); font-size: var(--fs-micro);
  text-transform: uppercase; letter-spacing: .06em; color: var(--text-3);
}
.topbar-link {
  color: var(--text-3); text-decoration: none; font-size: var(--fs-sm);
  font-weight: 500; transition: color var(--t);
}
.topbar-link:hover { color: var(--text); }

/* ── Layout ───────────────────────────────────────────────────────── */
main {
  max-width: 1400px; margin: 0 auto;
  padding: var(--sp-4) var(--sp-4) var(--sp-5);
  /* Landscape on a notched phone: viewport-fit=cover hands us the whole
     screen, insets included, and these are 0 everywhere else. */
  padding-left:  max(var(--sp-4), env(safe-area-inset-left));
  padding-right: max(var(--sp-4), env(safe-area-inset-right));
}

.page-header {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3); margin-bottom: var(--sp-3);
}
.page-title { font-size: var(--fs-lg); font-weight: 700; letter-spacing: -.02em; }

/* The header's three companions, promoted here from signals.css when a second
   page wanted them. A page title, a live indicator and a last-updated readout
   are not one page's furniture — they are the shape every live page in the
   estate has, and two copies of that shape drift. */
.ph-left { display: flex; align-items: center; gap: 10px; }
.ph-live {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--fs-xs); color: var(--text-3);
  text-transform: uppercase; letter-spacing: .09em; font-weight: 600;
}
.ph-updated {
  font-family: var(--font-mono); font-size: var(--fs-xs);
  color: var(--text-3); white-space: nowrap;
}

.section-title {
  font-family: var(--font-ui);
  font-size: var(--fs-micro); font-weight: 700; text-transform: uppercase;
  letter-spacing: .1em; color: var(--text-3);
  margin: var(--sp-5) 0 var(--sp-2); padding-bottom: var(--sp-2);
  border-bottom: 1px solid var(--line);
}

/* ── Buttons ──────────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  height: 34px; padding: 0 14px;
  background: var(--surface-2); border: 1px solid var(--line-2);
  color: var(--text); font-family: var(--font-ui);
  font-size: var(--fs-sm); font-weight: 600;
  border-radius: var(--r-sm); cursor: pointer;
  text-decoration: none; white-space: nowrap;
  transition: background var(--t), border-color var(--t), color var(--t);
}
.btn:hover { background: var(--surface-3); border-color: #333B4A; }
.btn:active { background: var(--surface-2); }
.btn-sm { height: 28px; padding: 0 10px; font-size: var(--fs-xs); }
/* The primary action carries the brand. Near-black ink on the blue fill
   measures 6.37:1, comfortably AA — white on this blue would be 2.84:1 and
   fail. The old treatment made the primary button the brightest thing on the
   page, which meant it competed with the P&L figures for the eye; the blue
   is louder as a signal and quieter in value. */
.btn-primary {
  background: var(--brand); border-color: var(--brand);
  color: var(--bg); font-weight: 700;
}
.btn-primary:hover {
  background: var(--brand-bright); border-color: var(--brand-bright);
  box-shadow: 0 4px 16px -6px rgba(91,140,255,.55);
}
.btn-primary:active { background: var(--brand); box-shadow: none; }
.btn-danger {
  background: var(--loss-dim); border-color: var(--loss-line); color: var(--loss);
}
.btn-danger:hover { background: rgba(255,77,109,.2); border-color: var(--loss); }

/* ── Fields — one input treatment everywhere, no per-form exceptions ──
   Matched by exclusion rather than by listing types: several inputs in the
   app omit `type` entirely (it defaults to text), and an `input[type=text]`
   selector silently skips those, leaving them unstyled. */
input:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=button]):not([type=hidden]),
select, textarea {
  height: 34px; padding: 0 10px; width: 100%;
  background: var(--surface-2); border: 1px solid var(--line-2);
  color: var(--text); border-radius: var(--r-sm);
  font-family: var(--font-ui); font-size: var(--fs-sm);
  transition: border-color var(--t), background var(--t);
}
select { cursor: pointer; }
textarea { height: auto; min-height: 72px; padding: 8px 10px; resize: vertical; }
input:hover, select:hover, textarea:hover { border-color: #333B4A; }
/* Fields get a ring rather than the global hard outline: an outline plus
   the field's own border read as a heavy double box. Still achromatic,
   still clearly visible. */
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: none; border-color: var(--text-2);
  box-shadow: 0 0 0 2px rgba(240,243,248,.16);
}
input::placeholder, textarea::placeholder { color: var(--text-3); }
input[type=date] { color-scheme: dark; }
option { background: var(--surface-2); color: var(--text); }
input[type=checkbox] { width: 15px; height: 15px; accent-color: var(--text-2); cursor: pointer; }

/* ── Panels & forms ───────────────────────────────────────────────── */
.card, .panel {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-lg);
  /* A whisper of depth to lift cards off the page ground — still flat by
     intent, just no longer coplanar with it. Transitions so any interactive
     card resolves state smoothly (the reduced-motion guard neutralises it). */
  box-shadow: var(--shadow-1);
  transition: box-shadow var(--t), border-color var(--t);
}
.panel { padding: var(--sp-4) var(--sp-4); }
.form-wrapper { max-width: 840px; margin: 0 auto; width: 100%; }
.form-wrapper.narrow { max-width: 440px; }

/* Auth: a genuinely centred screen rather than a top margin guess. */
.auth-shell {
  display: grid; place-items: center;
  min-height: calc(100vh - 150px);
  min-height: calc(100dvh - 150px);
}
.auth-card { width: 100%; max-width: 360px; }
.auth-head { margin-bottom: var(--sp-3); }
.auth-head .page-title { margin-bottom: 2px; }
.form-row {
  display: grid; grid-template-columns: repeat(2, 1fr);
  gap: var(--sp-3); margin-bottom: var(--sp-3);
}
.form-row.cols-1 { grid-template-columns: 1fr; }
.form-row.cols-3 { grid-template-columns: repeat(3, 1fr); }
.panel label, .field-label {
  display: flex; flex-direction: column; gap: 6px;
  font-size: var(--fs-micro); font-weight: 700;
  text-transform: uppercase; letter-spacing: .08em; color: var(--text-3);
}
.panel .actions { display: flex; gap: var(--sp-1); margin-top: var(--sp-3); }
/* A plain link sharing an .actions row with 34px buttons: give it the
   buttons' line so nothing sits baseline-adrift, without dressing it as
   a button. */
.actions .action-link {
  display: inline-flex; align-items: center; height: 34px;
  color: var(--text-2); text-decoration: none;
  font-size: var(--fs-sm); font-weight: 600;
  transition: color var(--t);
}
.actions .action-link:hover { color: var(--text); }
.form-hint { font-size: var(--fs-sm); color: var(--text-3); }

/* Inline form row — a real layout for "add a record" forms, which
   previously borrowed .filter-bar and inherited pill spacing meant for
   buttons. Fields size to content instead of stretching. */
.inline-form {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--sp-1);
  margin-bottom: var(--sp-3);
}
.inline-form input, .inline-form select { width: auto; min-width: 190px; }
.cell-form { display: inline-flex; align-items: center; gap: 6px; }
.cell-form input, .cell-form select { width: auto; }
.cell-actions { display: inline-flex; align-items: center; gap: 6px; justify-content: flex-end; flex-wrap: wrap; }
.input-sm { height: 28px !important; font-size: var(--fs-xs) !important; min-width: 150px; }

/* ── Shared page components ───────────────────────────────────────────
   Promoted from the per-page extra_head blocks (feeds, control, api-keys,
   cabinet) that all defined these byte-identically. */
.ctl-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3); flex-wrap: wrap;
}
.endpoint-box {
  font-family: var(--font-mono); font-size: var(--fs-sm);
  background: var(--surface-2); border: 1px solid var(--line-2);
  border-radius: var(--r-sm); padding: 10px 12px;
  overflow-x: auto; white-space: pre; user-select: all;
}
.panel-danger { border-color: var(--loss-line); }

/* ── Alerts ───────────────────────────────────────────────────────────
   Three variants, so pages stop hand-patching colour with inline styles. */
.alert {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
  padding: 10px 14px; margin-bottom: var(--sp-2);
  border: 1px solid var(--line-2); background: var(--surface-2);
  border-radius: var(--r-sm); font-size: var(--fs-sm); color: var(--text-2);
}
.alert-error   { background: var(--loss-dim); border-color: var(--loss-line); color: var(--loss); }
.alert-success { background: var(--win-dim);  border-color: var(--win-line);  color: var(--win);  }

/* ── Readout strip ────────────────────────────────────────────────────
   Replaces a row of boxed stat cards. One bordered container divided by
   hairlines: same figures, a fraction of the chrome. */
.stats-grid {
  display: flex; flex-wrap: wrap;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-lg); margin-bottom: var(--sp-3);
  overflow: hidden;
}
.stat-card {
  flex: 1 1 128px; min-width: 0;
  padding: 10px var(--sp-3);
  border-left: 1px solid var(--line);
}
.stat-card:first-child { border-left: none; }
.stat-card .label {
  font-size: var(--fs-micro); font-weight: 600; text-transform: uppercase;
  letter-spacing: .09em; color: var(--text-3); margin-bottom: 3px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.stat-card .value {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: var(--fs-num); font-weight: 700; letter-spacing: -.02em;
  line-height: 1.15;
}
.stat-card.green .value { color: var(--win); }
.stat-card.red   .value { color: var(--loss); }
.stat-card.yellow .value { color: var(--live); }
.stat-card.compact .value { font-size: 18px; }

/* ── Tables ───────────────────────────────────────────────────────── */
.table-wrap {
  overflow-x: auto; border: 1px solid var(--line);
  border-radius: var(--r-lg); background: var(--surface);
}
table { width: 100%; border-collapse: collapse; }
thead th {
  background: var(--surface); padding: 9px var(--sp-3);
  text-align: left; font-family: var(--font-ui);
  font-size: var(--fs-micro); font-weight: 700;
  text-transform: uppercase; letter-spacing: .09em;
  color: var(--text-3); border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
/* One row treatment. The previous theme stacked zebra striping, a hover
   tint and a state stripe on top of each other, which made scanning
   harder rather than easier — only the divider, hover and state remain. */
tbody tr { border-bottom: 1px solid var(--line); cursor: default; transition: background var(--t); }
tbody tr:last-child { border-bottom: none; }
tbody tr.clickable { cursor: pointer; }
tbody tr:hover { background: var(--surface-2); }
/* An achromatic left-accent on the row you're about to open — an affordance
   in white-alpha, since colour is reserved for P&L. On the first cell rather
   than the <tr> (inset shadows are unreliable on table rows). */
tbody tr.clickable:hover td:first-child { box-shadow: inset 2px 0 0 var(--text-2); }
td { padding: 11px var(--sp-3); white-space: nowrap; font-size: var(--fs-sm); }
td:last-child { text-align: right; }
.table-dense th, .table-dense td { padding: 7px 10px; font-size: var(--fs-sm); }
td.num, td .num, .mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* ── Badges ───────────────────────────────────────────────────────── */
.badge {
  display: inline-flex; align-items: center;
  height: 20px; padding: 0 8px;
  border-radius: var(--r-pill);
  font-family: var(--font-mono); font-size: var(--fs-micro);
  font-weight: 700; letter-spacing: .04em; white-space: nowrap;
  background: var(--surface-3); color: var(--text-2);
}
.badge.buy    { background: var(--win-dim);  color: var(--win);  }
.badge.sell   { background: var(--loss-dim); color: var(--loss); }
.badge.tp     { background: var(--win-dim);  color: var(--win);  }
.badge.sl     { background: var(--loss-dim); color: var(--loss); }
.badge.active { background: var(--live-dim); color: var(--live); }
.badge.na, .badge.unknown { background: var(--surface-3); color: var(--text-3); }

/* ── Filter controls ──────────────────────────────────────────────────
   Squared rather than pill-shaped: the pill is reserved for badges, so
   shape alone distinguishes "control" from "state". */
.filter-bar { display: flex; gap: 6px; margin-bottom: var(--sp-3); flex-wrap: wrap; align-items: center; }
.filter-btn {
  display: inline-flex; align-items: center; height: 28px; padding: 0 11px;
  border-radius: var(--r-sm); border: 1px solid var(--line-2);
  background: transparent; color: var(--text-3);
  font-family: var(--font-ui); font-size: var(--fs-sm); font-weight: 600;
  cursor: pointer; text-decoration: none; white-space: nowrap;
  transition: background var(--t), color var(--t), border-color var(--t);
}
.filter-btn:hover { color: var(--text-2); background: var(--surface-2); }
.filter-btn.active {
  color: var(--text); background: var(--surface-3); border-color: #333B4A;
}

/* ── Utilities ────────────────────────────────────────────────────── */
.num   { text-align: right; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.pos   { color: var(--win); }
.neg   { color: var(--loss); }
.muted { color: var(--text-3); }
.fw    { font-weight: 600; }
.ta-r  { text-align: right; }
.empty { text-align: center; padding: 48px; color: var(--text-3); font-size: var(--fs-sm); }
h3     { font-size: var(--fs-md); font-weight: 600; letter-spacing: -.01em; }

/* ── Live indicator ───────────────────────────────────────────────────
   Liveness is signalled by motion, not colour — colour belongs to P&L.
   The ring is a compositor-animated transform/opacity pulse; animating
   box-shadow instead repainted the dot on the main thread every frame. */
.live-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--text-3); flex-shrink: 0; position: relative;
}
.live-dot::after {
  content: ''; position: absolute; inset: -5px; border-radius: 50%;
  border: 1px solid var(--line-2);
  animation: live-pulse 2.4s ease-out infinite;
}
@keyframes live-pulse {
  0%   { transform: scale(.35); opacity: 1; }
  70%  { transform: scale(1);   opacity: 0; }
  100% { transform: scale(1);   opacity: 0; }
}

@keyframes shimmer { 0% { background-position: -260px 0; } 100% { background-position: 260px 0; } }
.skeleton {
  background: linear-gradient(90deg, var(--surface-2) 25%, var(--surface-3) 37%, var(--surface-2) 63%);
  background-size: 520px 100%; animation: shimmer 1.5s infinite;
  border-radius: var(--r-sm); color: transparent !important;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
}

/* ── Scrollbars ───────────────────────────────────────────────────── */
* { scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--line-2); border-radius: var(--r-sm);
  border: 2px solid transparent; background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: #39415a; background-clip: content-box; }

/* ── Small screens ────────────────────────────────────────────────── */
/* The nav (up to ~8 admin links) is what overflowed the viewport — not just
   on a phone but through the whole tablet band, since the full admin nav +
   logo + user menu is wider than ~900px. Let the nav shrink below its
   content width and scroll horizontally inside the bar instead of pushing
   the page sideways; the logo and user menu stay pinned. min-width:0 is the
   part that actually lets a flex item shrink past its content. */
@media (max-width: 960px) {
  body > header .logo { flex-shrink: 0; }
  body > header nav {
    min-width: 0; flex: 1 1 auto; flex-wrap: nowrap;
    overflow-x: auto; -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  body > header nav::-webkit-scrollbar { height: 0; }
  body > header nav a { white-space: nowrap; }
  .topbar-user { flex-shrink: 0; min-width: 0; }
}
/* The nav is a scroller from 960px down, but every scroll affordance above is
   switched off, so on a phone the links past the fold are invisible AND
   unhinted. A right-edge fade is the hint; snapping makes the flick land on a
   link instead of halfway through one. */
@media (max-width: 960px) {
  body > header nav {
    scroll-snap-type: x proximity;
    -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 24px), transparent);
    mask-image: linear-gradient(90deg, #000 calc(100% - 24px), transparent);
  }
  body > header nav a { scroll-snap-align: start; }
  /* The marketing header is three fixed items that fit any phone: exempt
     it from the scroller treatment so margin-left:auto keeps it pinned
     right, and drop the fade — with nothing to scroll it would only sit
     over the Get-started button's last 24px and dim it. */
  body > header nav.marketing-nav {
    flex: 0 1 auto;
    -webkit-mask-image: none; mask-image: none;
  }
}

@media (max-width: 720px) {
  body > header {
    gap: var(--sp-2);
    padding-left:  max(var(--sp-2), env(safe-area-inset-left));
    padding-right: max(var(--sp-2), env(safe-area-inset-right));
  }
  body > header .logo { font-size: 14px; }
  /* The username, the Password link and the Log out button eat ~146px of a
     375px header, leaving the nav a third of the bar. All three move into the
     account sheet; the trigger is one round button. */
  .topbar-user .who, .topbar-user .role,
  .topbar-user .topbar-link, .topbar-user form { display: none; }
  main { padding: var(--sp-3) var(--sp-2) var(--sp-4); }
  .form-row { grid-template-columns: 1fr; }
  .stat-card { flex-basis: 50%; }

  /* Anything below 16px makes iOS Safari zoom the page on focus, which is
     what makes these forms feel broken on a phone. */
  input:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=button]):not([type=hidden]),
  select, textarea { font-size: 16px; height: 44px; }
  textarea { height: auto; }
  .input-sm { height: 38px !important; font-size: 16px !important; min-width: 0; }

  /* A 190px floor in a 351px canvas gives four stacked controls with a ragged
     right edge. One column, full width, is both simpler and tidier. */
  .inline-form { display: grid; grid-template-columns: 1fr; gap: var(--sp-2); }
  .inline-form input, .inline-form select, .inline-form .btn { width: 100%; min-width: 0; }
  .inline-form label { width: 100%; }
  .cell-actions { width: 100%; justify-content: flex-start; }

  /* Manager and broker labels are free text, and .filter-btn is nowrap with no
     bound — one long label scrolls the whole page sideways. */
  /* display:block because text-overflow does not apply to a flex container —
     but that discards align-items, so the label needs a line-height to sit
     on the centre line instead of riding the top of the box. */
  .filter-btn { max-width: 100%; overflow: hidden; text-overflow: ellipsis;
                display: block; line-height: 26px; }
}

/* ── The sheet ────────────────────────────────────────────────────────
   Every menu in this app is an absolutely-positioned panel anchored to a
   button that sits in a wrapping toolbar, so on a phone the panel lands
   wherever its trigger happened to wrap to — off the right edge, off the
   left edge, or clipped by an ancestor's overflow. The fix removes the
   anchor rather than tuning it: below 720px a menu detaches and becomes a
   sheet on the bottom edge, where it is both thumb-reachable and incapable
   of leaving the viewport.

   The doubled class is deliberate. Page stylesheets live in extra_head and
   therefore load AFTER this one, so a single .tw-sheet would lose the
   cascade to .pop-panel/.lv-list at equal specificity. Doubling outranks
   them without reaching for !important.

   The query tests height-and-touch as well as width, because a phone held
   sideways is ~844px wide and ~390px tall. On width alone it read as a
   small desktop, the sheet never engaged, and the anchored panel opened
   off the edge of the screen — which is the exact bug the sheet exists to
   remove. A short window with a mouse keeps its anchored menus. */
@media (max-width: 720px), (pointer: coarse) and (max-height: 560px) {
  .tw-sheet.tw-sheet {
    position: fixed; left: 0; right: 0; top: auto;
    /* --tw-kb is the on-screen keyboard's height, published below. It is 0
       everywhere it is not set, so this reads as bottom:0 on a desktop. */
    bottom: var(--tw-kb, 0px);
    width: auto; min-width: 0; max-width: none;
    max-height: min(72vh,  calc(100vh  - var(--tw-kb, 0px) - 56px));
    max-height: min(72dvh, calc(100dvh - var(--tw-kb, 0px) - 56px));
    overflow-y: auto; overscroll-behavior: contain;
    margin: 0; z-index: 80;
    padding: 0 var(--sp-3) calc(var(--sp-3) + env(safe-area-inset-bottom));
    border: 0; border-top: 1px solid var(--line-2);
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    /* The primitive supplies its own surface. The popovers that adopt it
       happen to declare one already, which hid this — until the account
       sheet, which is not a popover, rendered as text on the scrim. */
    background: var(--surface-2);
    box-shadow: 0 -24px 60px -20px rgba(0,0,0,.85);
    animation: tw-sheet-in 200ms cubic-bezier(.2,.7,.2,1);
  }
  /* The handle is the spread bar — the device this app already uses to draw
     a spread to scale — rather than a generic iOS pill. Sticky, so it holds
     the top edge instead of scrolling away with the first row. */
  .tw-sheet.tw-sheet::before {
    content: ''; position: sticky; top: 0; z-index: 1;
    display: block; height: 14px; width: 100%;
    margin: 0 0 var(--sp-1);
    background: var(--surface-2)
                linear-gradient(var(--line-2), var(--line-2)) no-repeat center 8px / 36px 3px;
  }
  /* No new DOM and no change to any open/close handler: every one of these
     panels toggles the hidden attribute, so :has() can light the scrim. */
  body:has(.tw-sheet:not([hidden]))::before {
    content: ''; position: fixed; inset: 0; z-index: 70;
    background: rgba(6,7,10,.62);
    -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
  }
  .tw-sheet.tw-sheet > .pop-row:last-child { margin-bottom: 0; }
}
@keyframes tw-sheet-in { from { transform: translateY(100%); } to { transform: none; } }

/* ── Stacked tables ───────────────────────────────────────────────────
   A table is a desktop instrument. On a phone the same rows read far better
   as the Market Watch strip this app already invented for its densest view:
   the identity cell becomes a mono header, and every other cell becomes one
   label-left / figure-right line. Opt in with .stack on the .table-wrap and
   a data-label on each <td> after the first. */
@media (max-width: 720px) {
  /* max-height:none because several of these wrappers are their own capped
     scrollers on desktop (a 560px feed, a 420px log). Stacked, that would
     put a scroll box of cards inside the page scroll — the nested scrolling
     this pattern exists to remove. */
  .table-wrap.stack { overflow-x: visible; overflow-y: visible; max-height: none;
                      border: 0; background: none; border-radius: 0; }
  .table-wrap.stack thead {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip-path: inset(50%);
  }
  .table-wrap.stack table, .table-wrap.stack tbody,
  .table-wrap.stack tr, .table-wrap.stack td { display: block; width: 100%; }
  .table-wrap.stack tbody tr {
    background: var(--surface); border: 1px solid var(--line);
    border-radius: var(--r-lg); box-shadow: var(--shadow);
    margin-bottom: var(--sp-2); overflow: hidden;
  }
  .table-wrap.stack tbody tr:hover { background: var(--surface); }
  .table-wrap.stack td {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--sp-2); padding: 9px var(--sp-3);
    white-space: normal; text-align: right; border: 0;
  }
  .table-wrap.stack td + td { border-top: 1px solid var(--line); }
  .table-wrap.stack td::before {
    content: attr(data-label); flex: 0 0 auto; text-align: left;
    font-size: var(--fs-micro); font-weight: 700; letter-spacing: .09em;
    text-transform: uppercase; color: var(--text-3);
  }
  /* The first cell is the row's identity — it names the row, so a label
     above it would only repeat itself. */
  .table-wrap.stack td:first-child {
    font-family: var(--font-mono); font-weight: 700; font-size: var(--fs-md);
    justify-content: flex-start; text-align: left;
    background: var(--surface-alt); border-bottom: 1px solid var(--line);
  }
  .table-wrap.stack td:first-child::before,
  .table-wrap.stack td.empty::before { content: none; }
  .table-wrap.stack td.empty { display: block; text-align: center; }
  .table-wrap.stack td:last-child { text-align: right; }
  /* A cell whose content needs the whole width — an action cluster, a
     password field — puts its label on its own line and lets the controls
     have the row, instead of squeezing them against the right edge. */
  .table-wrap.stack td.cell-act { display: block; text-align: left; }
  .table-wrap.stack td.cell-act::before { display: block; margin-bottom: 6px; }
  .table-wrap.stack td.cell-act .cell-actions,
  .table-wrap.stack td.cell-act .cell-form { width: 100%; flex-wrap: wrap; }
  .table-wrap.stack td.cell-act .btn { flex: 1 1 auto; }
}

/* A finger is not a mouse pointer: 28px controls are a third under the 44px
   minimum. Keyed on the pointer, not the width, so a touch laptop counts. */
@media (pointer: coarse) {
  .btn { height: 44px; padding: 0 16px; }
  .btn-sm { height: 38px; padding: 0 12px; }
  /* Doubled for the same reason .tw-sheet is: page stylesheets live in
     extra_head and load after this one, so a single class loses the
     cascade to things like .sig-seg .filter-btn and leaves the most-tapped
     controls on the page at 26px while everything around them grows. */
  .filter-btn.filter-btn { height: 38px; padding: 0 14px; line-height: 36px; }
  .pop-btn.pop-btn { height: 38px; }
}

/* ── The account sheet's trigger ──────────────────────────────────────── */
.topbar-acct {
  display: none; align-items: center; justify-content: center;
  width: 34px; height: 34px; flex-shrink: 0;
  border-radius: var(--r-pill); border: 1px solid var(--border-2);
  background: var(--surface-alt); color: var(--text);
  font-family: var(--font-ui); font-size: var(--fs-sm); font-weight: 700;
  cursor: pointer; padding: 0;
}
.topbar-acct:hover { background: var(--surface-3); }
.acct-sheet { display: none; }
/* Both reveals must sit AFTER the two display:none rules above. Media queries
   add no specificity, so at equal weight it is source order that decides —
   putting these up in the main phone block instead would lose to the base
   rules down here and the button would never appear. */
@media (max-width: 720px) {
  .topbar-acct { display: inline-flex; }
  .acct-sheet  { display: block; }
}
.acct-sheet .who {
  font-weight: 700; font-size: var(--fs-md);
  display: flex; align-items: baseline; gap: 8px; padding: 0 var(--sp-1) var(--sp-2);
}
.acct-sheet .role {
  font-family: var(--font-mono); font-size: var(--fs-micro);
  text-transform: uppercase; letter-spacing: .06em; color: var(--text-3);
}
.acct-sheet a, .acct-sheet button {
  display: flex; align-items: center; width: 100%; height: 46px;
  padding: 0 var(--sp-1); border: 0; border-top: 1px solid var(--border);
  background: none; color: var(--text); text-align: left;
  font-family: var(--font-ui); font-size: var(--fs-md); font-weight: 600;
  text-decoration: none; cursor: pointer;
}
.sr-only {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
/* Impersonation banner. Amber is the estate's "live/temporary — pay
   attention" hue; on it, dark ink and a dark button read as the inverse of
   the normal chrome, so an admin viewing as a user can never lose track of
   it and always has one click back to themselves. */
.imp-banner {
  display: flex; align-items: center; gap: var(--sp-1);
  padding: var(--sp-1) var(--sp-2);
  background: var(--live); color: #14110a;
  font-family: var(--font-ui); font-size: var(--fs-sm); font-weight: 600;
}
.imp-banner .imp-text { flex: 1 1 auto; min-width: 0; }
.imp-banner strong { font-weight: 800; }
.imp-banner .imp-dot {
  width: 8px; height: 8px; border-radius: 50%; flex: none;
  background: #14110a;
}
.imp-banner form { margin: 0; }
.imp-banner .btn {
  background: #14110a; color: var(--live); border: 1px solid #14110a;
}
@media (max-width: 720px) { .imp-banner { flex-wrap: wrap; } }

/* A proposed symbol mapping, above the dropdown that would apply it.
   Deliberately not inside the select: a pre-selected option looks like the
   current state, and a proposal must never be mistaken for a mapping already
   in force. The tier badge reuses the existing semantic set — the panel
   already spends red on "Unmapped", so red here reads as "needs a look"
   rather than as a fourth vocabulary. */
.sym-sug {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  margin-bottom: 6px;
}
.sym-sug .note-xs { flex-basis: 100%; margin-top: 2px; }
