/* Franchise Intelligence — design system.
 *
 * One stylesheet, three surfaces: the public marketing site, the auth pages,
 * and the authenticated application. They share tokens and primitives so the
 * product feels like one thing, but they do not share layout — a landing page
 * and a data workspace want opposite things from their whitespace.
 *
 * Everything is driven by custom properties. Change a token here, not a rule
 * further down.
 */

/* ==========================================================================
   Tokens
   ========================================================================== */

:root {
  /* Neutrals — a slightly cool grey ramp, so the blue accent sits naturally */
  --n-0:#ffffff; --n-25:#fbfcfe; --n-50:#f6f8fb; --n-100:#eef2f7; --n-150:#e5ebf3;
  /* --n-500 is the single source of --ink-3, the secondary ink used by .muted,
     .tiny, every hint and every KPI sub-line. At #68758a it cleared AA only on
     pure white: 4.39:1 on the page ground and 4.15:1 on --bg-sunken, both under
     the 4.5:1 that 11.5-13px text needs. #626e82 is the smallest step that
     clears all three grounds (5.16 / 4.85 / 4.59). */
  --n-200:#d9e1ea; --n-300:#bcc7d4; --n-400:#8e9bab; --n-500:#626e82;
  --n-600:#4c586b; --n-700:#36414f; --n-800:#232c39; --n-900:#141b26; --n-950:#0c111a;

  /* Brand */
  --brand-50:#eef4ff; --brand-100:#dbe7fe; --brand-200:#bed3fe; --brand-300:#92b5fd;
  --brand-400:#5e8ffa; --brand-500:#2f6df6; --brand-600:#1a51e6; --brand-700:#163fc3;
  --brand-800:#18379e; --brand-900:#19337d; --brand-950:#14204a;

  /* Semantic */
  --ok-50:#e9f8f1; --ok-500:#0b7a58; --ok-600:#08654a; --ok-700:#064e39;
  --warn-50:#fdf6e7; --warn-500:#a56a07; --warn-600:#8a5806;
  --bad-50:#fdefec; --bad-500:#c0442c; --bad-600:#a33824;

  /* Applied roles — remapped wholesale in dark mode */
  --bg:var(--n-50); --bg-raised:var(--n-0); --bg-sunken:var(--n-100);
  --ink:var(--n-900); --ink-2:var(--n-700); --ink-3:var(--n-500); --ink-4:var(--n-400);
  --line:var(--n-200); --line-soft:var(--n-150);
  --accent:var(--brand-500); --accent-hover:var(--brand-600); --accent-soft:var(--brand-50);
  --accent-ink:var(--brand-700);
  /* Solid fills that carry white text. Kept separate from --accent because
     dark mode brightens the accent for legibility on dark backgrounds, which
     would drop white-on-accent button text below the 4.5:1 contrast floor. */
  --accent-solid:var(--brand-500); --accent-solid-hover:var(--brand-600);
  --ok:var(--ok-500); --ok-soft:var(--ok-50);
  --warn:var(--warn-500); --warn-soft:var(--warn-50);
  /* --warn is the fill colour; on --warn-soft it measures 4.18:1, which is
     below AA for the 11.5px badge text that is its only use as a foreground.
     Kept separate for the same reason --accent-solid is: the swatch and the
     text on it cannot be the same value. */
  --warn-ink:var(--warn-600);
  --bad:var(--bad-500); --bad-soft:var(--bad-50);
  --focus:var(--brand-400);

  /* Type */
  --font:ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
  --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
  --t-xs:11.5px; --t-sm:13px; --t-base:14.5px; --t-md:16px; --t-lg:18px;
  --t-xl:22px; --t-2xl:28px; --t-3xl:36px; --t-4xl:46px;

  /* Space — 4px base */
  --s-1:4px; --s-2:8px; --s-3:12px; --s-4:16px; --s-5:20px; --s-6:24px;
  --s-8:32px; --s-10:40px; --s-12:48px; --s-16:64px; --s-20:80px; --s-24:96px;

  /* Radius & elevation */
  --r-sm:6px; --r:9px; --r-md:12px; --r-lg:16px; --r-xl:22px; --r-full:999px;
  --sh-1:0 1px 2px rgba(16,24,40,.05);
  --sh-2:0 1px 3px rgba(16,24,40,.07), 0 1px 2px rgba(16,24,40,.05);
  --sh-3:0 4px 12px rgba(16,24,40,.08), 0 2px 4px rgba(16,24,40,.04);
  --sh-4:0 12px 32px rgba(16,24,40,.10), 0 4px 8px rgba(16,24,40,.05);
  --sh-5:0 24px 60px rgba(16,24,40,.14);

  --sidebar-w:236px;
  --topbar-h:60px;
  --ease:cubic-bezier(.22,.61,.36,1);

  /* Interactive control heights. Tokenised because they have to grow together
     on touch — a mouse can hit a 38px target, a thumb on the 63-field location
     survey cannot do it repeatedly without errors. */
  --control-h:38px;
  --control-h-sm:31px;
  --control-h-lg:46px;
}

/* Touch input, at any screen size — a tablet needs this as much as a phone,
   and a narrow desktop window does not. */
@media (pointer: coarse) {
  :root {
    --control-h:44px;
    --control-h-sm:36px;
    --control-h-lg:50px;
  }
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg:var(--n-950); --bg-raised:#141b26; --bg-sunken:#0a0f16;
    --ink:#f2f5f9; --ink-2:#cdd6e1; --ink-3:#95a2b3; --ink-4:#6f7d90;
    --line:#28323f; --line-soft:#1e2731;
    --accent:var(--brand-400); --accent-hover:var(--brand-300);
    --accent-soft:rgba(94,143,250,.13); --accent-ink:var(--brand-300);
    --accent-solid:var(--brand-600); --accent-solid-hover:var(--brand-500);
    --ok:#3ec08d; --ok-soft:rgba(62,192,141,.13);
    --warn:#dda43c; --warn-soft:rgba(221,164,60,.13); --warn-ink:#dda43c;
    --bad:#ef7059; --bad-soft:rgba(239,112,89,.13);
    --sh-1:0 1px 2px rgba(0,0,0,.4); --sh-2:0 1px 3px rgba(0,0,0,.45);
    --sh-3:0 4px 12px rgba(0,0,0,.5); --sh-4:0 12px 32px rgba(0,0,0,.55);
    --sh-5:0 24px 60px rgba(0,0,0,.6);
  }
}

:root[data-theme="dark"] {
  --bg:var(--n-950); --bg-raised:#141b26; --bg-sunken:#0a0f16;
  --ink:#f2f5f9; --ink-2:#cdd6e1; --ink-3:#95a2b3; --ink-4:#6f7d90;
  --line:#28323f; --line-soft:#1e2731;
  --accent:var(--brand-400); --accent-hover:var(--brand-300);
  --accent-soft:rgba(94,143,250,.13); --accent-ink:var(--brand-300);
  --accent-solid:var(--brand-600); --accent-solid-hover:var(--brand-500);
  --ok:#3ec08d; --ok-soft:rgba(62,192,141,.13);
  --warn:#dda43c; --warn-soft:rgba(221,164,60,.13); --warn-ink:#dda43c;
  --bad:#ef7059; --bad-soft:rgba(239,112,89,.13);
  --sh-1:0 1px 2px rgba(0,0,0,.4); --sh-2:0 1px 3px rgba(0,0,0,.45);
  --sh-3:0 4px 12px rgba(0,0,0,.5); --sh-4:0 12px 32px rgba(0,0,0,.55);
  --sh-5:0 24px 60px rgba(0,0,0,.6);
}

/* ==========================================================================
   Base
   ========================================================================== */

*,*::before,*::after { box-sizing:border-box; }
html { -webkit-text-size-adjust:100%; scroll-behavior:smooth; }
body {
  margin:0; background:var(--bg); color:var(--ink-2);
  font:var(--t-base)/1.6 var(--font);
  -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale;
}
h1,h2,h3,h4 { margin:0; color:var(--ink); font-weight:650; letter-spacing:-.021em; line-height:1.2; }
h1 { font-size:var(--t-3xl); letter-spacing:-.032em; }
h2 { font-size:var(--t-xl); }
h3 { font-size:var(--t-md); }
h4 { font-size:var(--t-base); }
p { margin:0 0 var(--s-3); }
a { color:var(--accent); text-decoration:none; }
a:hover { text-decoration:underline; }
strong,b { color:var(--ink); font-weight:640; }
code { font-family:var(--mono); font-size:.9em; }
img,svg { max-width:100%; }
::selection { background:var(--accent-soft); color:var(--accent-ink); }

:focus-visible {
  outline:2px solid var(--focus); outline-offset:2px; border-radius:var(--r-sm);
}

.hidden,[hidden] { display:none !important; }
.muted { color:var(--ink-3); }
.tiny { font-size:var(--t-xs); color:var(--ink-3); }
.small { font-size:var(--t-sm); }
.nowrap { white-space:nowrap; }
.center { text-align:center; }
.num { font-variant-numeric:tabular-nums; }
.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;
}

.eyebrow {
  font-size:var(--t-xs); font-weight:700; letter-spacing:.13em;
  text-transform:uppercase; color:var(--accent); margin:0 0 var(--s-2);
}
.hint { font-size:var(--t-xs); font-weight:400; color:var(--ink-3); margin-left:var(--s-2); }

/* --------------------------------------------------------------------------
   Workspace city context

   Decorative city art belongs beside the analytical workspace, not over the
   maps and charts the customer is paying to inspect. The fixed 1.55:1 art well
   gives square source illustrations a deliberate editorial crop on desktop,
   then becomes a compact strip on a phone. */
.city-context {
  display:grid; grid-template-columns:minmax(0,1.08fr) minmax(280px,.92fr);
  min-height:218px; margin:0 0 var(--s-8); overflow:hidden;
  border:1px solid var(--line); border-radius:var(--r-lg); background:var(--bg-raised);
  box-shadow:var(--sh-2);
}
.city-context-copy { display:flex; flex-direction:column; justify-content:center; padding:var(--s-8); }
/* Below the page title, not above it. At --t-2xl this decorative heading
   outranked the h1 naming the page. */
.city-context-copy h2 { max-width:20ch; font-size:var(--t-xl); }
.city-context-copy p:last-child { max-width:55ch; margin:var(--s-3) 0 0; color:var(--ink-3); }
.city-context-art { position:relative; min-height:218px; overflow:hidden; background:var(--accent-soft); }
.city-context-art::after {
  content:""; position:absolute; inset:0; pointer-events:none;
  background:linear-gradient(90deg,var(--bg-raised) 0%,transparent 26%,rgba(11,37,69,.10) 100%);
}
/* Absolutely positioned so the artwork cannot set the row height.
   The files are square (1254x1254). In flow at a 531px column width that is a
   531px intrinsic height, which silently overrode the 218px min-height above
   and spent 84% of the first screen on decoration before any analysis. Out of
   flow, the row is sized by the copy beside it and `cover` crops the art to
   fit — which is what `object-fit` was there to do all along. */
.city-context-art img {
  position:absolute; inset:0; width:100%; height:100%;
  object-fit:cover; object-position:center 46%; display:block; opacity:.92;
}
.city-context-fallback {
  display:none; position:absolute; inset:0; align-items:flex-end; padding:var(--s-5);
  color:var(--accent-ink); font-size:var(--t-xl); font-weight:700; letter-spacing:-.03em;
  background:
    radial-gradient(circle at 78% 24%,rgba(94,143,250,.28) 0 12%,transparent 12.5%),
    linear-gradient(135deg,var(--accent-soft),var(--bg-sunken));
}
.city-context.is-missing-art .city-context-art img { display:none; }
.city-context.is-missing-art .city-context-fallback { display:flex; }

@media (max-width:720px) {
  .city-context { grid-template-columns:1fr; min-height:0; margin:var(--s-4) 0 var(--s-5); }
  .city-context-art { order:-1; min-height:144px; }
  .city-context-art::after { background:linear-gradient(180deg,transparent 48%,var(--bg-raised) 100%); }
  .city-context-copy { padding:var(--s-5); }
  .city-context-copy h2 { font-size:var(--t-xl); }
}

@media (prefers-reduced-motion: reduce) {
  *,*::before,*::after { animation-duration:.01ms !important; transition-duration:.01ms !important; }
  html { scroll-behavior:auto; }
}

/* ==========================================================================
   Primitives — buttons
   ========================================================================== */

.btn {
  display:inline-flex; align-items:center; justify-content:center; gap:var(--s-2);
  font:600 var(--t-sm)/1 var(--font); padding:0 var(--s-4); height:var(--control-h);
  border:1px solid var(--line); border-radius:var(--r); background:var(--bg-raised);
  color:var(--ink); cursor:pointer; white-space:nowrap; text-decoration:none;
  transition:background .14s var(--ease), border-color .14s var(--ease),
             transform .14s var(--ease), box-shadow .14s var(--ease);
}
.btn:hover { background:var(--bg-sunken); border-color:var(--n-300); text-decoration:none; }
.btn:active { transform:translateY(.5px); }
.btn:disabled,.btn[aria-disabled="true"] { opacity:.5; cursor:not-allowed; transform:none; }

.btn-primary {
  background:var(--accent-solid); border-color:var(--accent-solid); color:#fff; box-shadow:var(--sh-1);
}
.btn-primary:hover {
  background:var(--accent-solid-hover); border-color:var(--accent-solid-hover); box-shadow:var(--sh-2);
}

/* Theme toggle. Shows the theme it will switch *to*: a moon while light, a sun
   while dark. Both icons ship in the markup and CSS picks — no JS, so the
   right one is correct on first paint. */
[data-theme-toggle] svg {
  width:17px; height:17px; display:block;
  fill:none; stroke:currentColor; stroke-width:1.7;
  stroke-linecap:round; stroke-linejoin:round;
}
[data-theme-toggle] .ic-sun { display:none; }
[data-theme-toggle] .ic-moon { display:block; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) [data-theme-toggle] .ic-sun { display:block; }
  :root:not([data-theme="light"]) [data-theme-toggle] .ic-moon { display:none; }
}
:root[data-theme="dark"] [data-theme-toggle] .ic-sun { display:block; }
:root[data-theme="dark"] [data-theme-toggle] .ic-moon { display:none; }
:root[data-theme="light"] [data-theme-toggle] .ic-sun { display:none; }
:root[data-theme="light"] [data-theme-toggle] .ic-moon { display:block; }

.btn-ghost { background:transparent; border-color:transparent; color:var(--ink-2); }
.btn-ghost:hover { background:var(--bg-sunken); border-color:transparent; }
.btn-danger { background:var(--bad); border-color:var(--bad); color:#fff; }
.btn-danger:hover { background:var(--bad-600); border-color:var(--bad-600); }
.btn-lg { height:var(--control-h-lg); padding:0 var(--s-6); font-size:var(--t-md); border-radius:var(--r-md); }
.btn-sm { height:var(--control-h-sm); padding:0 var(--s-3); font-size:var(--t-xs); }
.btn-block { width:100%; }

/* ==========================================================================
   Primitives — form controls
   ========================================================================== */

.field { display:flex; flex-direction:column; gap:var(--s-2); min-width:0; }
.field > .label,
label.label {
  font-size:var(--t-xs); font-weight:650; letter-spacing:.045em;
  text-transform:uppercase; color:var(--ink-3);
}
.field .help { font-size:var(--t-xs); color:var(--ink-3); }
.field .error { font-size:var(--t-xs); color:var(--bad); font-weight:600; }

input[type=text],input[type=email],input[type=tel],input[type=password],
input[type=number],input[type=search],select,textarea {
  font:400 var(--t-sm)/1.4 var(--font); color:var(--ink);
  background:var(--bg-raised); border:1px solid var(--line);
  border-radius:var(--r); padding:0 var(--s-3); height:var(--control-h); width:100%; min-width:0;
  transition:border-color .14s var(--ease), box-shadow .14s var(--ease);
  -webkit-appearance:none; appearance:none;
}
textarea { height:auto; padding:var(--s-3); resize:vertical; min-height:84px; }
select {
  padding-right:var(--s-8);
  background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='none' stroke='%2368758a' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round' d='M1 1.5 6 6.5l5-5'/%3E%3C/svg%3E");
  background-repeat:no-repeat; background-position:right var(--s-3) center;
}
input:focus,select:focus,textarea:focus {
  outline:none; border-color:var(--accent);
  box-shadow:0 0 0 3px var(--accent-soft);
}
input::placeholder,textarea::placeholder { color:var(--ink-4); }
input:disabled,select:disabled,textarea:disabled {
  background:var(--bg-sunken); color:var(--ink-3); cursor:not-allowed;
}
/* `margin:0` is load-bearing: the UA stylesheet gives a range input 2px of
   horizontal margin, so `width:100%` inside a grid cell overflows its track
   by exactly 4px — enough to put a scrollbar on the section. */
/* A bare range renders 16px tall, which is half the 32px tap target the audit
   requires on a phone — and on the desktop toolbars it left the slider sitting
   on the floor of a row whose selects are 38px, so the two controls beside each
   other never shared a centre line. The track does not move; the control grows
   around it to the height everything else in a toolbar row already has. */
input[type=range] {
  height:var(--control-h); min-height:32px; margin:0; padding:0; border:0;
  background:transparent; accent-color:var(--accent);
}
input[type=checkbox],input[type=radio] {
  width:auto; height:auto; min-width:0; accent-color:var(--accent); margin:0;
}

.form-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:var(--s-4); }
.form-grid-1 { grid-template-columns:minmax(0,1fr); }
.form-grid-3 { grid-template-columns:repeat(3,minmax(0,1fr)); }
.form-grid .span-2 { grid-column:span 2; }
@media (max-width:840px) {
  .form-grid,.form-grid-3 { grid-template-columns:minmax(0,1fr); }
  .form-grid .span-2 { grid-column:auto; }
}

.check {
  display:flex; align-items:flex-start; gap:var(--s-3); padding:var(--s-3) var(--s-4);
  border:1px solid var(--line); border-radius:var(--r); background:var(--bg-raised);
  font-size:var(--t-sm); color:var(--ink-2); cursor:pointer; text-transform:none;
  letter-spacing:0; font-weight:400; transition:border-color .14s var(--ease);
}
.check:hover { border-color:var(--accent); }
.check input { margin-top:3px; flex:0 0 auto; }
.check.on { border-color:var(--accent); background:var(--accent-soft); }

/* ==========================================================================
   Primitives — surfaces
   ========================================================================== */

.card {
  background:var(--bg-raised); border:1px solid var(--line);
  border-radius:var(--r-md); box-shadow:var(--sh-1);
}
.card-pad { padding:var(--s-5); }
.card-head {
  display:flex; align-items:center; justify-content:space-between; gap:var(--s-4);
  padding:var(--s-4) var(--s-5); border-bottom:1px solid var(--line-soft);
}
.card-head h3 { font-size:var(--t-base); }
/* Heading for a block *inside* a card body — a card has one .card-head, but a
   body can hold two distinct jobs that each need naming. */
.sub-head {
  font-size:var(--t-xs); font-weight:650; letter-spacing:.045em;
  text-transform:uppercase; color:var(--ink-2); margin:0 0 var(--s-1);
}
/* The same heading where a block of content, not a card edge, sits above it. */
.sub-head-spaced { margin-top:var(--s-5); }

/* A module that has not been run yet. Reads as "waiting for you", rather than
   as a broken panel: an em-dash alone in a full-width card looks like a
   rendering failure, which is what several of these were doing. */
.awaiting {
  display:flex; align-items:center; justify-content:center; gap:var(--s-2);
  min-height:96px; padding:var(--s-5);
  border:1px dashed var(--line); border-radius:var(--r);
  color:var(--ink-3); font-size:var(--t-sm); text-align:center;
}
.awaiting::before { content:"○"; opacity:.5; }
.card-body { padding:var(--s-5); }
.card-foot {
  padding:var(--s-4) var(--s-5); border-top:1px solid var(--line-soft);
  background:var(--bg-sunken); border-radius:0 0 var(--r-md) var(--r-md);
}

.stack { display:flex; flex-direction:column; gap:var(--s-4); }
.stack-sm { gap:var(--s-2); }
.stack-lg { gap:var(--s-6); }
.row { display:flex; align-items:center; gap:var(--s-3); flex-wrap:wrap; }
.row-between { display:flex; align-items:center; justify-content:space-between; gap:var(--s-4); flex-wrap:wrap; }
.grid-2 { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:var(--s-5); }
/* Two cards of very different heights — an eight-field form beside a one-line
   "run this first" note — stretched the empty one to 450px of blank panel.
   Aligning to the top lets each card be its own size; add `.grid-even` back
   where matched heights actually matter. */
.grid-2, .grid-3, .grid-auto { align-items:start; }
.grid-even { align-items:stretch; }
.grid-3 { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:var(--s-5); }
.grid-auto { display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:var(--s-5); }
@media (max-width:980px) { .grid-2,.grid-3 { grid-template-columns:minmax(0,1fr); } }

.divider { height:1px; background:var(--line-soft); border:0; margin:var(--s-5) 0; }

/* --------------------------------------------------------------------------
   Naming a band of content — the spine /app/data reads by, and the one the
   workspace had none of.

   These three lived in the DATA & COVERAGE block, which read as though they
   belonged to one page. They do not: a heading above a group of cards, a
   heading inside a card, and a proportion drawn as a track are the three
   things every analytical surface needs, and the workspace was reinventing
   each of them per tab — an <h3> with an inline font-size here, a bare <b>
   there, a percentage printed as text somewhere else. Moved here so the two
   surfaces cannot drift apart again.
   -------------------------------------------------------------------------- */

/* Above a group: what the group is, and what it is for. The right-hand slot
   carries the group's own metadata — a count, a timestamp, a caveat. */
.section-label-row { display:flex; justify-content:space-between; align-items:flex-end; gap:var(--s-4); margin:0 0 var(--s-4); }
.section-label-row h2 { font-size:var(--t-xl); }
.section-label-row p { color:var(--ink-3); font-size:var(--t-sm); margin:var(--s-1) 0 0; }
/* Inside a card: an eyebrow, the card's own title, and a right-hand slot. */
.card-heading { display:flex; justify-content:space-between; gap:var(--s-4); margin-bottom:var(--s-2); }
.card-heading .eyebrow { margin-bottom:var(--s-1); }
.card-heading h2 { font-size:var(--t-lg); }
/* A proportion, drawn. 6px is the reading height: tall enough to carry a
   colour at a glance, short enough that a column of them does not read as a
   bar chart competing with the numbers beside it. */
.metric-bar { height:6px; overflow:hidden; border-radius:var(--r-full); background:var(--bg-sunken); }
.metric-bar > i { display:block; height:100%; min-width:0; border-radius:inherit; background:var(--accent); }
/* Label on the left, track in a fixed column, reading right-aligned against a
   fixed gutter — so a column of them scans down three straight edges whatever
   the labels say. The one measured row geometry in the product; /app/data's
   evidence-quality list and the workspace's rails are the same component. */
.tier-row { display:grid; grid-template-columns:minmax(0,1fr) 98px 34px; align-items:center; gap:var(--s-3); }
.tier-label { display:flex; justify-content:space-between; gap:var(--s-2); min-width:0; font-size:var(--t-sm); color:var(--ink-2); }
.tier-label span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.tier-label strong { font-size:var(--t-xs); }
.tier-bars { display:grid; gap:var(--s-4); margin:var(--s-5) 0; }

/* ==========================================================================
   Primitives — badges, pills, callouts
   ========================================================================== */

.badge {
  display:inline-flex; align-items:center; gap:var(--s-1); padding:3px var(--s-2);
  border-radius:var(--r-full); font-size:var(--t-xs); font-weight:650;
  background:var(--bg-sunken); color:var(--ink-3); white-space:nowrap;
}
.badge-accent { background:var(--accent-soft); color:var(--accent-ink); }
.badge-ok { background:var(--ok-soft); color:var(--ok); }
.badge-warn { background:var(--warn-soft); color:var(--warn-ink); }
.badge-bad { background:var(--bad-soft); color:var(--bad); }
.badge-dot::before {
  content:""; width:6px; height:6px; border-radius:50%; background:currentColor; flex:0 0 auto;
}

/* How sure we are of a figure, beside the figure. A bar rather than a number
   alone because the reader is comparing rows, and 0.62 against 0.35 is a
   comparison you have to do arithmetic for. The number stays for anyone who
   wants it, and for anyone reading with a screen reader. */
.confidence-cell { display:flex; align-items:center; gap:var(--s-2); white-space:nowrap; }
.confidence-bar {
  display:inline-block; width:56px; height:5px; flex:0 0 auto;
  border-radius:var(--r-full); background:var(--bg-sunken); overflow:hidden;
}
.confidence-bar > i { display:block; height:100%; background:var(--accent); border-radius:inherit; }
.confidence-value { font-size:var(--t-xs); color:var(--ink-3); font-variant-numeric:tabular-nums; }

.callout {
  border:1px solid var(--line); border-left:3px solid var(--accent);
  background:var(--bg-sunken); border-radius:0 var(--r) var(--r) 0;
  padding:var(--s-4) var(--s-5); font-size:var(--t-sm);
}
.callout p:last-child { margin-bottom:0; }
.callout-ok { border-left-color:var(--ok); background:var(--ok-soft); }
.callout-warn { border-left-color:var(--warn); background:var(--warn-soft); }
.callout-bad { border-left-color:var(--bad); background:var(--bad-soft); }
/* The "what this means" reading that closes an analytical panel. Quieter than a
   plain callout on purpose: it appears in every panel, and a box that shouts in
   every panel is a box readers learn to skip. */
.callout b:first-child { color:var(--ink); }
/* Spacing lives here rather than on each call site, which was emitting
   `style="margin:12px 0"` — a 12px top margin that pushed the ranking panel's
   first line 12px below the profile panel's, so two columns that start at the
   same y began on different lines. */
.callout-reading { margin:var(--s-4) 0 var(--s-3); }
.callout-reading:first-child { margin-top:0; }
/* A reading that closes a table has nothing else separating the two. */
table + .callout, table + .callout-reading { margin-top:var(--s-4); }

/* ==========================================================================
   Primitives — tables
   ========================================================================== */

.table-wrap { overflow-x:auto; -webkit-overflow-scrolling:touch; }
table { width:100%; border-collapse:collapse; font-size:var(--t-sm); }
th,td { text-align:left; padding:var(--s-3); border-bottom:1px solid var(--line-soft); vertical-align:top; }
thead th {
  font-size:var(--t-xs); font-weight:650; letter-spacing:.06em; text-transform:uppercase;
  color:var(--ink-3); border-bottom:1px solid var(--line); white-space:nowrap;
  position:sticky; top:0; background:var(--bg-raised); z-index:1;
}
td.num,th.num { text-align:right; font-variant-numeric:tabular-nums; white-space:nowrap; }
/* Row headers carry the row's name to a screen reader. The browser default
   bolds them; the table already signals hierarchy with thead, so keep the
   body weight and let scope do the work. */
tbody th { font-weight:inherit; }
tbody tr:last-child td, tbody tr:last-child th { border-bottom:0; }
tbody tr.clickable { cursor:pointer; }
tbody tr.clickable:hover { background:var(--bg-sunken); }

/* ==========================================================================
   Primitives — feedback
   ========================================================================== */

.spinner {
  display:inline-block; width:14px; height:14px; flex:0 0 auto;
  border:2px solid var(--line); border-top-color:var(--accent);
  border-radius:50%; animation:spin .7s linear infinite; vertical-align:-2px;
}
.spinner-lg { width:26px; height:26px; border-width:2.5px; }
@keyframes spin { to { transform:rotate(360deg); } }

.loading { display:flex; align-items:center; gap:var(--s-3); color:var(--ink-3); font-size:var(--t-sm); }

.empty {
  text-align:center; padding:var(--s-12) var(--s-5); color:var(--ink-3);
}
.empty .icon { font-size:var(--t-2xl); opacity:.5; margin-bottom:var(--s-3); color:var(--accent); }
.empty .icon svg {
  width:38px; height:38px; margin:0 auto; display:block;
  fill:none; stroke:currentColor; stroke-width:1.4;
  stroke-linecap:round; stroke-linejoin:round;
}
.empty .icon-strong { opacity:1; }
/* Secondary identifiers inside a table cell — the short id or timestamp that
   distinguishes otherwise identical rows. */
.row-id,.row-sub {
  display:block; font-size:var(--t-xs); color:var(--ink-4); font-weight:400; margin-top:2px;
}
.row-id { font-family:var(--mono); letter-spacing:.02em; }

.empty-title { font-size:var(--t-lg); color:var(--ink); }
.empty-lede { font-size:var(--t-base); color:var(--ink-3); max-width:42ch; margin:0 auto var(--s-4); }
.empty-actions { display:flex; justify-content:center; gap:var(--s-3); flex-wrap:wrap; margin-top:var(--s-6); }
.empty h3 { color:var(--ink-2); margin-bottom:var(--s-2); }
.empty p { max-width:400px; margin:0 auto var(--s-4); font-size:var(--t-sm); }
.empty-illustration { display:block; object-fit:contain; margin:0 auto var(--s-3); }
.empty-illustration-wide { width:min(100%, 240px); height:auto; }
.empty-illustration-square { width:112px; height:112px; }

.skeleton {
  background:linear-gradient(90deg,var(--bg-sunken) 25%,var(--line-soft) 37%,var(--bg-sunken) 63%);
  background-size:400% 100%; animation:shimmer 1.3s ease infinite; border-radius:var(--r-sm);
}
@keyframes shimmer { 0% { background-position:100% 50%; } 100% { background-position:0 50%; } }

#toast-host {
  position:fixed; bottom:var(--s-6); left:50%; transform:translateX(-50%);
  z-index:9999; display:flex; flex-direction:column; gap:var(--s-2); align-items:center;
  pointer-events:none;
}
.toast {
  background:var(--n-900); color:#fff; padding:var(--s-3) var(--s-5);
  border-radius:var(--r); font-size:var(--t-sm); font-weight:500;
  box-shadow:var(--sh-4); max-width:min(92vw,440px);
  animation:toast-in .22s var(--ease);
}
.toast-bad { background:var(--bad-600); }
.toast-ok { background:var(--ok-600); }
@keyframes toast-in { from { opacity:0; transform:translateY(10px); } }
:root[data-theme="dark"] .toast { background:var(--n-800); border:1px solid var(--line); }

/* ==========================================================================
   Score visuals — shared by workspace and reports
   ========================================================================== */

.score-pill {
  display:inline-block; min-width:40px; text-align:center; padding:2px var(--s-2);
  border-radius:var(--r-sm); font-weight:650; font-size:var(--t-xs);
  font-variant-numeric:tabular-nums;
}
.s-excellent { background:var(--ok-soft); color:var(--ok); }
.s-good { background:var(--accent-soft); color:var(--accent-ink); }
.s-moderate { background:var(--warn-soft); color:var(--warn); }
.s-weak { background:var(--bad-soft); color:var(--bad); }
.s-poor { background:var(--bg-sunken); color:var(--ink-3); }

.bar { height:6px; border-radius:var(--r-full); background:var(--bg-sunken); overflow:hidden; min-width:70px; }
.bar > i { display:block; height:100%; background:var(--accent); border-radius:var(--r-full); }
.bar.neg > i { background:var(--bad); }
/* How sure we are, never how good it is.
 *
 * The design system's first rule is that fit gets hue and confidence does not:
 * two colour ramps in one panel are averaged by the eye into a single "how
 * good is this row", which is the collapse the encoding exists to prevent. A
 * confidence meter beside a fit meter in the same accent blue is exactly that
 * collapse, so confidence draws in ink. */
.bar-neutral > i { background:var(--ink-4); }

/* A percentile row: the label and its reading on one line, the bar beneath.
 *
 * Twelve of these stack in the area profile, which is why it is a component
 * rather than twelve copies of the same inline styles. Those had drifted to a
 * 7px rhythm off the 4px grid, put no space at all between a reading and its
 * own bar, and left the first row touching the KPI tiles above it — so the
 * block read as one striped slab rather than twelve measurements. */
.index-bars { margin-top:var(--s-5); }
.index-bar + .index-bar { margin-top:var(--s-3); }
.index-bars-sm .index-bar + .index-bar { margin-top:var(--s-2); }
.index-bar .index-head {
  display:flex; align-items:baseline; justify-content:space-between;
  gap:var(--s-3); font-size:var(--t-xs); margin-bottom:var(--s-1);
}
.index-bar .index-label { color:var(--ink-2); }
/* tabular-nums: the readings are right-aligned against the bar's right edge, so
   "100th" and "81st" have to occupy the same width down the column. */
.index-bar .index-read {
  color:var(--ink-3); text-align:right; font-variant-numeric:tabular-nums;
}
.index-bar .index-read b { font-weight:650; color:var(--ink-2); }
/* The tick marking the locality rank has to escape the track vertically, which
   .bar's overflow:hidden forbade; the fill keeps its own pill radius instead. */
.index-bar .bar { position:relative; overflow:visible; }
.index-bar .tick {
  position:absolute; top:-3px; bottom:-3px; width:2px; border-radius:1px;
  background:var(--ink-2); opacity:.55;
}

/* --s-2, not --s-1: the ranking panel wraps seventeen of these into a block
   six rows deep, and a 4px gap left the rows all but touching. */
.chips { display:flex; gap:var(--s-2); flex-wrap:wrap; margin-top:var(--s-1); }
.chip {
  font-size:var(--t-xs); padding:3px var(--s-2); border-radius:var(--r-full);
  background:var(--bg-sunken); color:var(--ink-3);
}
.chip.pos { background:var(--ok-soft); color:var(--ok); }
.chip.neg { background:var(--bad-soft); color:var(--bad); }

/* auto-fill, not auto-fit: the strip renders a variable number of tiles, and
   auto-fit would collapse the unused tracks and stretch two tiles across the
   whole page. auto-fill keeps every tile the same width whatever the count. */
/* --s-4, matching /app/data, which was already overriding this to 16px in
   its markup. The golden page does not move; every other .kpi-row moves 4px
   toward it. */
.kpi-row { display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:var(--s-4); }
/* A 190px minimum is right for a full-width strip and wrong inside a card that
   is half the content width: the area profile's three tiles fell to 2 + 1, with
   the third stranded beside a hole. auto-fit rather than auto-fill so the tiles
   share the row they do fit on instead of leaving empty tracks. */
.card-body .kpi-row { grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); }
/* A column, so the reading sits on the floor of the tile rather than a fixed
   distance below its own label. Tiles in a grid stretch to a common height, and
   "Market rent ₹/sqft" wraps to two lines at a width where "Evidence
   confidence" still fits on one — which put three numbers that are read across
   on three different baselines. */
.kpi {
  background:var(--bg-raised); border:1px solid var(--line);
  border-radius:var(--r-md); padding:var(--s-4); box-shadow:var(--sh-1);
  display:flex; flex-direction:column;
}
.kpi .l {
  font-size:var(--t-xs); font-weight:650; letter-spacing:.06em;
  text-transform:uppercase; color:var(--ink-3);
}
.kpi .v {
  font-size:var(--t-xl); font-weight:660; letter-spacing:-.025em;
  color:var(--ink); margin-top:auto; padding-top:var(--s-1);
  font-variant-numeric:tabular-nums;
}
.kpi .s { font-size:var(--t-xs); color:var(--ink-3); margin-top:2px; }

/* Banded KPI tiles.
   A component class, not a page id. This block used to be scoped to
   `#exec-kpis`, which meant /app/data — same `.kpi exec-kpi` markup, different
   page — silently got none of it, and the identical component rendered at two
   sizes depending on which URL you were on.

   What the bands buy: label, value and sub each occupy a fixed row, so those
   three baselines line up across every tile even when one label wraps to two
   lines and its neighbour does not. /app/data gets that alignment for free
   because its five labels happen to be a uniform length; a row of seven does
   not, so the mechanism is kept — at /app/data's dimensions.

   Dimensions come from the base `.kpi` (16px padding, 22px value). The former
   override ran 18px 20px with a 30px value and a 166px min-height, justified
   by a comment about long numerator/denominator values wrapping. Measured
   against the longest real value, `1,491/ 1,849`, at the /data treatment: 114px
   of text in a 186px box, identical height at 186px and 600px probe widths.
   It does not wrap. */
.kpi-row { align-items:stretch; }
.kpi-banded { display:flex; flex-direction:column; overflow:hidden; }
.kpi-banded .l { min-height:2.45em; display:flex; align-items:flex-start; line-height:1.25; }
.kpi-banded .v {
  display:flex; align-items:baseline; gap:6px; white-space:nowrap; line-height:1.2;
}
.kpi-banded .s { min-height:2.8em; line-height:1.4; }
.kpi-value-number { font-variant-numeric:tabular-nums; }
.kpi-value-total { font-size:var(--t-sm); font-weight:600; letter-spacing:0; color:var(--ink-3); }
.exec-kpi-progress {
  height:4px; margin-top:auto; border-radius:var(--r-full); overflow:hidden;
  background:var(--bg-sunken);
}
.exec-kpi-progress > i { display:block; height:100%; background:var(--accent); border-radius:inherit; }
.kpi-warn .exec-kpi-progress > i { background:var(--warn); }
.kpi-bad .exec-kpi-progress > i { background:var(--bad); }
.exec-kpi-progress.is-empty { visibility:hidden; }

@media (max-width:760px) {
  #exec-kpis { grid-template-columns:repeat(auto-fill,minmax(170px,1fr)); }
  #exec-kpis .exec-kpi { min-height:150px; padding:16px; }
  #exec-kpis .exec-kpi .v { font-size:27px; }
}

/* --------------------------------------------------------------------------
   Metric cards — Proximity, Urban Fabric, and the catchment's fabric read.
   These three panels grew independently and drifted: one rendered a bare
   table, one a table plus a paragraph, and the catchment a stack of cards with
   their own inline styles. Side by side the labels sat at three different
   heights, badges rode the text baseline instead of the label's, and a long
   metric name pushed its own card taller than its neighbours so the row of
   values no longer scanned across.

   One grid, one card, fixed vertical rhythm: label block, value block, note
   block, each with a floor so every card in a row lines up whatever it holds.
   -------------------------------------------------------------------------- */

/* One travel band of a catchment: its name, and the metrics inside it.
   Separated by a rule rather than by a box — three raised cards inside the
   raised rail was the third surface level DESIGN_SPECS §5 rules out, and read
   as a double border down both edges of the column. */
.band + .band { margin-top:var(--s-5); padding-top:var(--s-5); border-top:1px solid var(--line-soft); }
/* Catchment band header: swatch, band name and the travel note on one
   baseline. It was three inline-styled spans that wrapped unevenly once a
   locality name made the panel narrower. */
.band-head { display:flex; align-items:center; gap:var(--s-2); margin-bottom:7px; flex-wrap:wrap; }
.band-head .sw { display:inline-block; width:14px; height:11px; border-radius:3px; flex:0 0 auto; }
/* The grid's own --s-4 top margin sits under a heading that already carries
   7px; the band owns that gap. */
.band .metric-grid { margin-top:var(--s-3); }
.band:last-of-type .metric-grid { margin-bottom:0; }

/* Panel heading, shared by every docked inspector. The three panels each
   carried their own inline font-size and colour, which is how they ended up
   at three different sizes. */
.panel-title { text-transform:none; font-size:var(--t-base); color:var(--ink); margin:0; }
.panel-sub { font-size:var(--t-xs); color:var(--ink-3); margin:2px 0 0; }

/* The figures in a rail that are not numbers — a cluster label, a count of
   observations and their sources. They were rows of a `<td class="num">`
   table, right-aligned against numerals they are not comparable with, and
   `white-space:normal` in the rail meant a two-word label wrapped under its
   own value. A definition list says what they are: a term and its reading. */
.panel-facts {
  display:grid; gap:var(--s-2); margin:var(--s-4) 0 0;
  padding-top:var(--s-3); border-top:1px solid var(--line-soft);
  font-size:var(--t-xs);
}
.panel-facts dt { color:var(--ink-3); }
.panel-facts dd { margin:2px 0 0; color:var(--ink-2); }

/* `align-items:stretch` equalises cards within a row, but each row still sized
   to its own tallest note — Urban Fabric's seven-card rail measured 153, 153,
   169 and 187px down its four rows, which is the raggedness the reference page
   never shows. `grid-auto-rows:1fr` equalises the rows themselves, so one grid
   has one card height however long an individual note runs. Gap is the tile gap
   (--s-4); 12px was the last grid in the app on neither the tile nor the panel
   step. */
.metric-grid {
  display:grid; grid-template-columns:repeat(auto-fit,minmax(148px,1fr));
  grid-auto-rows:1fr; gap:var(--s-4); align-items:stretch; margin:var(--s-4) 0;
}
.metric-card {
  display:flex; flex-direction:column; min-width:0;
  background:var(--bg-raised); border:1px solid var(--line);
  border-radius:var(--r-md); padding:var(--s-3) var(--s-4);
}
/* The head is a row so the badge sits opposite the label rather than after it,
   and `min-height` is in em of the label's own line so a one-line and a
   two-line label still put their values on the same line. */
.metric-card .m-head { min-height:2.4em; line-height:1.2; }
.metric-card .m-label {
  font-size:var(--t-xs); font-weight:650; letter-spacing:.05em;
  text-transform:uppercase; color:var(--ink-3);
  /* Wraps rather than truncates: these are metric names, and "Separate poc…"
     is not a metric name. The head's floor absorbs the second line.
     `break-word`, not `anywhere`: `anywhere` split "CONNECTIONS" across two
     lines as "CONNECTION / S". Labels are kept short enough not to need it. */
  overflow-wrap:break-word;
}
/* Pinned to the foot by `margin-top:auto`, so a row of cards puts every badge
   on one line however tall the notes above them are. */
.metric-card .m-badge { margin-top:auto; padding-top:var(--s-2); }
.metric-card .m-badge .badge { max-width:100%; white-space:normal; }
.metric-card .m-value {
  display:flex; align-items:baseline; gap:var(--s-1); min-height:30px;
  margin-top:var(--s-2); font-size:22px; font-weight:660; letter-spacing:-.02em;
  color:var(--ink); font-variant-numeric:tabular-nums; line-height:1;
}
.metric-card .m-unit { font-size:var(--t-xs); font-weight:600; color:var(--ink-3); letter-spacing:0; }
.metric-card .m-note {
  font-size:var(--t-xs); color:var(--ink-3); margin-top:var(--s-2);
  min-height:2.6em; line-height:1.35;
}
/* A row of cards with nothing to say under any of them should not reserve
   three empty lines; the floor only applies when at least one card uses it. */
.metric-grid.is-flush .metric-card .m-note { min-height:0; }
.metric-card.m-accent { border-left:3px solid var(--accent); }
.metric-card.m-warn { border-left:3px solid var(--warn); }
.metric-card.m-warn .m-value { color:var(--warn); }

/* The docked inspector is a fixed-width column beside a map. Without a floor
   on the track the cards collapse to one per row at the exact width the map
   is most useful, and without the max the same markup on a wide report page
   stretched four cards across 900px of whitespace. */
.map-side .metric-grid { grid-template-columns:repeat(auto-fit,minmax(120px,1fr)); }

/* Nesting inside the docked rail.
 *
 * Catchment drew three raised .cards inside the raised .map-side, which is the
 * third surface level DESIGN_SPECS §5 rules out — "one raised surface, two
 * levels, no third" — and read as a double border down both edges. The rules
 * that made those inner cards read as sunken have gone with them: the bands
 * are `.band` now, separated by a hairline rather than boxed, and every figure
 * in them is a `.metric-card` like every other rail's. `ui_audit.js` C7 fails
 * a `.card` nested in a rail, so there is no path back to the markup those
 * rules were compensating for. */
.map-side .callout { padding:var(--s-3) var(--s-4); }
/* Two lines is the right floor at card width and one line short at rail width:
   "Reachable past barriers" takes three lines in a 131px track, which pushed
   its numeral 17px below the numeral beside it — the audit's L4, and the one
   thing a reader is actually comparing across the row. The rail reserves the
   third line so the values sit on one line whatever the label does. Scoped to
   the rail, so a wide report page does not pay for space it never needs. */
.map-side .metric-card .m-head { min-height:3.6em; }
/* The rail is two columns wide, so an odd number of metrics left the last card
   at half width with an empty cell beside it — read as a card that failed to
   load rather than as the end of a list. It takes the row instead. */
.map-side .metric-grid > .metric-card:last-child:nth-child(odd) { grid-column:1 / -1; }
@media (max-width:640px) { .metric-grid { grid-template-columns:repeat(auto-fit,minmax(132px,1fr)); } }

/* Tiles that represent something the user has to act on. The left rule is the
   only differentiator — a full tint would make a status strip look like an
   error state. */
.kpi-accent { border-left:3px solid var(--accent); }
.kpi-accent .v { color:var(--accent-ink); }
.kpi-bad { border-left:3px solid var(--bad); }
.kpi-bad .v { color:var(--bad); }
.kpi-warn { border-left:3px solid var(--warn); }
.kpi-warn .v { color:var(--warn); }

/* Financial simulator: its result host begins as an `.awaiting` flexbox. Once
   a forecast exists, switch it back to a normal document flow and give the
   metrics a compact, intentionally balanced grid. */
#fin-results.finance-results { display:block; padding:0; border:0; min-height:0; color:inherit; text-align:left; }
#fin-results.finance-results::before { content:none; }
.finance-kpi-grid {
  display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:var(--s-4);
  grid-auto-rows:1fr; margin-bottom:var(--s-5);
}
.finance-kpi-grid .kpi { min-height:106px; padding:var(--s-4); display:flex; flex-direction:column; }
/* The same label floor the banded tiles use. Without it "Year-1 cash return"
   wrapped to two lines and dropped its numeral 19px below "Payback" and
   "5-yr IRR" beside it — three returns a reader compares across, on three
   baselines. The floor is in em of the label's own line, so it holds at any
   type size. */
.finance-kpi-grid .kpi .l { min-height:2.45em; display:flex; align-items:flex-start; line-height:1.25; }
.finance-kpi-grid .kpi .v { margin-top:var(--s-2); font-size:var(--t-lg); white-space:nowrap; }
.finance-kpi-grid .kpi .s { margin-top:auto; }
.finance-detail { border-top:1px solid var(--line-soft); padding-top:var(--s-4); }
/* A .callout in everything but name, and it had drifted: 12/16 padding against
   the callout's 16/20, and four rounded corners against the callout's two — so
   the results panel stacked two verdict boxes that agreed on nothing but the
   accent rule down their left edge. Same geometry, its own tint. */
.finance-verdict {
  margin:0 0 var(--s-4); padding:var(--s-4) var(--s-5);
  border:1px solid var(--line); border-left:3px solid var(--accent);
  border-radius:0 var(--r) var(--r) 0;
  background:var(--accent-soft); color:var(--ink-2);
  font-size:var(--t-sm); line-height:1.5;
}
.finance-detail table { width:100%; min-width:420px; }
.finance-section-title { margin-top:var(--s-5); }
.finance-output-grid { margin-top:var(--s-5); }
@media (max-width:620px) {
  .finance-kpi-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
  .finance-kpi-grid .kpi { min-height:96px; padding:var(--s-3); }
  .finance-kpi-grid .kpi .v { font-size:var(--t-base); }
}

/* ==========================================================================
   PUBLIC SURFACE
   ========================================================================== */

.pub-nav {
  position:sticky; top:0; z-index:50; background:color-mix(in srgb,var(--bg-raised) 88%,transparent);
  backdrop-filter:saturate(180%) blur(12px); -webkit-backdrop-filter:saturate(180%) blur(12px);
  border-bottom:1px solid var(--line-soft);
}
.pub-nav-inner {
  max-width:1200px; margin:0 auto; padding:0 var(--s-6); height:var(--topbar-h);
  display:flex; align-items:center; justify-content:space-between; gap:var(--s-6);
}
.pub-links { display:flex; align-items:center; gap:var(--s-1); }
.pub-links a {
  padding:var(--s-2) var(--s-3); border-radius:var(--r-sm); font-size:var(--t-sm);
  font-weight:550; color:var(--ink-2);
}
.pub-links a:hover { background:var(--bg-sunken); color:var(--ink); text-decoration:none; }
.pub-links a.on { color:var(--accent); }
@media (max-width:860px) { .pub-links { display:none; } }
/* Below this the nav is brand + CTA only, and the two stop fitting: a signed-in
   visitor gets "Open dashboard", which is wide enough to push the row past the
   nav and scroll the marketing site sideways. The wordmark is the part that can
   go — the mark still identifies the site, which is why the markup keeps them
   as separate elements. Scoped to .pub-nav so the sidebar and auth pages, which
   have room for it, keep theirs. */
@media (max-width:560px) { .pub-nav .brand-name { display:none; } }

.brand { display:inline-flex; align-items:center; gap:var(--s-3); color:var(--ink); }
.brand:hover { text-decoration:none; }
.brand-mark {
  width:32px; height:32px; border-radius:var(--r-md); flex:0 0 auto;
  display:inline-flex; align-items:center; justify-content:center;
  background:rgba(255,255,255,0.95);
  border:1px solid rgba(15,23,42,0.08);
  box-shadow:0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04);
  padding:2px;
  overflow:hidden;
  transition:transform .15s ease, box-shadow .15s ease;
}
.brand-mark img {
  width:100%; height:100%; object-fit:contain; display:block;
}
.brand:hover .brand-mark {
  transform:scale(1.04);
  box-shadow:0 2px 8px rgba(37,99,235,0.22);
}
.brand-name { font-weight:650; font-size:var(--t-base); letter-spacing:-.02em; color:var(--ink); }

/* The hero's decorative glow is deliberately inset -20% so it bleeds past the
   hero's sides; what was never intended was for that bleed to widen the
   document and let the whole marketing site scroll sideways on a phone.
   Clipping x only keeps the vertical bleed, which is the part you can see. */
.pub-main { max-width:1200px; margin:0 auto; padding:0 var(--s-6); overflow-x:clip; }
.pub-section { padding:var(--s-20) 0; }
.pub-section-tight { padding:var(--s-12) 0; }

.hero { padding:var(--s-24) 0 var(--s-20); text-align:center; position:relative; }
.hero::before {
  content:""; position:absolute; inset:-10% -20% auto; height:520px; z-index:-1;
  background:radial-gradient(ellipse 60% 100% at 50% 0%,var(--accent-soft),transparent 70%);
  pointer-events:none;
}
.hero h1 {
  font-size:clamp(34px,5.5vw,var(--t-4xl)); line-height:1.08; max-width:16ch;
  margin:0 auto var(--s-5);
}
.hero .lede {
  font-size:var(--t-lg); color:var(--ink-3); max-width:58ch; margin:0 auto var(--s-8);
  line-height:1.6;
}
/* That bottom margin exists to separate the lede from .hero-cta. On a hero
   that has no CTA it is dead space, and it stacks with the hero's own bottom
   padding and the next section's top padding into a visible hole. */
.hero .lede:last-child { margin-bottom:0; }
.hero-cta { display:flex; gap:var(--s-3); justify-content:center; flex-wrap:wrap; }
.hero-note { margin-top:var(--s-5); font-size:var(--t-sm); color:var(--ink-3); }
.eyebrow { margin:0 0 var(--s-3); color:var(--accent); font-size:var(--t-xs); font-weight:750; letter-spacing:.12em; text-transform:uppercase; }
.hero-product { padding-top:var(--s-20); }
.hero-product h1 { max-width:15ch; }
.proof-strip { display:flex; justify-content:center; flex-wrap:wrap; gap:var(--s-3) var(--s-6); margin-top:var(--s-8); color:var(--ink-3); font-size:var(--t-sm); }
.proof-strip span { padding-left:var(--s-4); border-left:1px solid var(--line); }
.proof-strip span:first-child { border-left:0; padding-left:0; }
.proof-strip b { color:var(--ink); font-variant-numeric:tabular-nums; }
.problem-section { display:grid; grid-template-columns:minmax(0,.9fr) minmax(0,1.1fr); gap:var(--s-12); align-items:center; border-top:1px solid var(--line-soft); }
.problem-copy h2,.report-copy h2,.pricing-callout h2,.brands-callout h2 { font-size:var(--t-2xl); margin-bottom:var(--s-4); }
.problem-copy > p:last-child,.report-copy > p:last-child,.pricing-callout p,.brands-callout p { color:var(--ink-3); line-height:1.65; }
.blind-spot-grid { display:grid; gap:var(--s-3); }
.blind-spot-grid article { position:relative; padding:var(--s-5) var(--s-5) var(--s-5) 76px; border:1px solid var(--line); border-radius:var(--r-md); background:var(--bg-raised); }
.blind-spot-grid .number { position:absolute; left:var(--s-5); top:var(--s-5); color:var(--accent); font:700 var(--t-sm)/1 var(--mono); }
.blind-spot-grid h3 { font-size:var(--t-base); margin-bottom:var(--s-1); }
.blind-spot-grid p { margin:0; color:var(--ink-3); font-size:var(--t-sm); }
.svc-card-featured { border-color:var(--accent); box-shadow:var(--sh-3); }
.process-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:1px; padding:0; margin:0; list-style:none; background:var(--line); border:1px solid var(--line); border-radius:var(--r-lg); overflow:hidden; }
.process-grid li { min-height:190px; padding:var(--s-6); background:var(--bg-raised); }
.process-grid span { color:var(--accent); font:700 var(--t-xs)/1 var(--mono); letter-spacing:.08em; }
.process-grid h3 { font-size:var(--t-md); margin:var(--s-5) 0 var(--s-2); }
.process-grid p { color:var(--ink-3); font-size:var(--t-sm); margin:0; }
.report-section { display:grid; grid-template-columns:1fr 1fr; gap:var(--s-12); padding:var(--s-12); background:var(--bg-sunken); border:1px solid var(--line-soft); border-radius:var(--r-xl); }
.text-link { color:var(--accent); font-size:var(--t-sm); font-weight:650; }
.report-index { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:var(--s-3); align-content:center; }
.report-index span { display:flex; flex-direction:column; gap:var(--s-2); padding:var(--s-4); background:var(--bg-raised); border:1px solid var(--line); border-radius:var(--r); color:var(--accent); font:650 var(--t-xs)/1 var(--mono); }
.report-index b { color:var(--ink); font:600 var(--t-sm)/1.3 var(--font); }
.trust-section { border-top:1px solid var(--line-soft); }
.pricing-callout,.brands-callout { display:flex; justify-content:space-between; gap:var(--s-8); align-items:center; padding:var(--s-10); border:1px solid var(--line); border-radius:var(--r-xl); background:var(--bg-raised); }
.pricing-callout > div:first-child,.brands-callout > div { max-width:62ch; }
.price-panel { min-width:220px; display:grid; gap:var(--s-2); padding:var(--s-5); border-radius:var(--r-md); background:var(--accent-soft); }
.price-panel strong { font-size:var(--t-3xl); line-height:1; letter-spacing:-.04em; color:var(--accent-ink); }
.price-panel span { color:var(--ink-3); font-size:var(--t-xs); margin-bottom:var(--s-2); }
@media (max-width:860px) { .problem-section,.report-section { grid-template-columns:1fr; } .process-grid { grid-template-columns:repeat(2,minmax(0,1fr)); } .pricing-callout,.brands-callout { align-items:flex-start; flex-direction:column; } }
@media (max-width:560px) { .pub-main { padding:0 var(--s-4); } .proof-strip { justify-content:flex-start; } .proof-strip span { border-left:0; padding-left:0; width:100%; } .process-grid { grid-template-columns:1fr; } .report-section,.pricing-callout,.brands-callout { padding:var(--s-6); } .report-index { grid-template-columns:1fr; } }

.split-cards { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:var(--s-5); }
@media (max-width:860px) { .split-cards { grid-template-columns:minmax(0,1fr); } }
.svc-card {
  background:var(--bg-raised); border:1px solid var(--line); border-radius:var(--r-lg);
  padding:var(--s-8); box-shadow:var(--sh-2); display:flex; flex-direction:column;
  transition:border-color .18s var(--ease), transform .18s var(--ease), box-shadow .18s var(--ease);
}
.svc-card:hover { border-color:var(--accent); transform:translateY(-2px); box-shadow:var(--sh-4); }
.svc-card .tag {
  font-size:var(--t-xs); font-weight:700; letter-spacing:.12em; text-transform:uppercase;
  color:var(--accent); margin-bottom:var(--s-3);
}
/* Both levels: on the home page these cards sit under an h2 section heading so
   they are h3, on pricing they are the page's top-level sections so they are
   h2. The card should look identical either way. */
.svc-card h2,.svc-card h3 { font-size:var(--t-xl); margin-bottom:var(--s-2); }
.svc-card .q { color:var(--ink-3); font-size:var(--t-md); font-style:italic; margin-bottom:var(--s-5); }
.svc-card ul { list-style:none; margin:0 0 var(--s-6); padding:0; flex:1; }
.svc-card li {
  padding:var(--s-2) 0 var(--s-2) var(--s-6); position:relative; font-size:var(--t-sm);
  border-bottom:1px solid var(--line-soft);
}
.svc-card li:last-child { border-bottom:0; }
.svc-card li::before {
  content:""; position:absolute; left:var(--s-2); top:50%; width:7px; height:7px;
  margin-top:-3.5px; border-radius:50%; background:var(--accent);
}
.svc-card .price {
  display:flex; align-items:baseline; gap:var(--s-2); margin-bottom:var(--s-5);
  padding-top:var(--s-5); border-top:1px solid var(--line-soft);
}
.svc-card .price .amt { font-size:var(--t-2xl); font-weight:680; color:var(--ink); letter-spacing:-.03em; }
.svc-card .price .per { font-size:var(--t-sm); color:var(--ink-3); }

.feature-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); gap:var(--s-8) var(--s-6); }
.feature h3 { font-size:var(--t-base); margin-bottom:var(--s-2); }
.feature p { font-size:var(--t-sm); color:var(--ink-3); margin:0; }
.feature .ic {
  width:36px; height:36px; border-radius:var(--r); background:var(--accent-soft);
  color:var(--accent-ink); display:grid; place-items:center; margin-bottom:var(--s-3);
}
.feature .ic svg {
  width:19px; height:19px; display:block;
  fill:none; stroke:currentColor; stroke-width:1.6;
  stroke-linecap:round; stroke-linejoin:round;
}

.steps { counter-reset:step; display:grid; gap:var(--s-5); }
.step { display:grid; grid-template-columns:36px 1fr; gap:var(--s-4); align-items:start; }
.step::before {
  counter-increment:step; content:counter(step);
  width:36px; height:36px; border-radius:50%; background:var(--accent-soft);
  color:var(--accent-ink); display:grid; place-items:center; font-weight:700; font-size:var(--t-sm);
}
.step h4 { margin-bottom:var(--s-1); }
.step p { font-size:var(--t-sm); color:var(--ink-3); margin:0; }

.section-head { text-align:center; max-width:64ch; margin:0 auto var(--s-12); }
.section-head h2 { font-size:var(--t-2xl); margin-bottom:var(--s-3); }
.section-head p { color:var(--ink-3); font-size:var(--t-md); margin:0; }

.faq-item { border-bottom:1px solid var(--line-soft); padding:var(--s-5) 0; }
.faq-item summary {
  cursor:pointer; font-weight:600; color:var(--ink); font-size:var(--t-md);
  list-style:none; display:flex; justify-content:space-between; gap:var(--s-4);
}
.faq-item summary::-webkit-details-marker { display:none; }
.faq-item summary::after { content:"+"; color:var(--ink-3); font-weight:400; font-size:var(--t-lg); }
.faq-item[open] summary::after { content:"−"; }
.faq-item p { margin:var(--s-3) 0 0; color:var(--ink-3); font-size:var(--t-sm); max-width:76ch; }

.cta-band {
  background:linear-gradient(135deg,var(--brand-950),var(--brand-800) 60%,var(--brand-600));
  border-radius:var(--r-xl); padding:var(--s-16) var(--s-8); text-align:center; color:#fff;
  box-shadow:var(--sh-4);
}
.cta-band h2 { color:#fff; font-size:var(--t-2xl); margin-bottom:var(--s-3); }
.cta-band p { color:rgba(255,255,255,.78); max-width:52ch; margin:0 auto var(--s-6); }
.cta-band .btn { background:#fff; border-color:#fff; color:var(--brand-800); }
.cta-band .btn:hover { background:rgba(255,255,255,.9); border-color:rgba(255,255,255,.9); }

.pub-footer {
  border-top:1px solid var(--line-soft); background:var(--bg-raised); margin-top:var(--s-20);
}
.pub-footer-inner {
  max-width:1200px; margin:0 auto; padding:var(--s-12) var(--s-6) var(--s-8);
  display:grid; grid-template-columns:2fr 1fr 1fr 1fr; gap:var(--s-8);
}
@media (max-width:860px) { .pub-footer-inner { grid-template-columns:1fr 1fr; } }
/* h2, not h4: these label the footer's link groups and were the last heading
   level on the page, so h4 after page-level h2s skipped a level on all ten
   public pages. Styled small — the outline level and the type scale are
   independent decisions. */
.pub-footer .foot-col-title {
  font-size:var(--t-xs); font-weight:650; text-transform:uppercase; letter-spacing:.09em;
  color:var(--ink-3); margin-bottom:var(--s-3);
}
.pub-footer ul { list-style:none; margin:0; padding:0; display:grid; gap:var(--s-2); }
.pub-footer a { color:var(--ink-2); font-size:var(--t-sm); }
.pub-footer a:hover { color:var(--accent); }
.pub-footer-base {
  max-width:1200px; margin:0 auto; padding:var(--s-5) var(--s-6);
  border-top:1px solid var(--line-soft); font-size:var(--t-xs); color:var(--ink-3);
  display:flex; justify-content:space-between; gap:var(--s-4); flex-wrap:wrap;
}

/* Legal / long-form prose */
.prose { max-width:74ch; margin:0 auto; padding:var(--s-12) 0 var(--s-20); }
.prose h1 { font-size:var(--t-2xl); margin-bottom:var(--s-2); }
.prose .updated { color:var(--ink-3); font-size:var(--t-sm); margin-bottom:var(--s-8); }
.prose h2 { font-size:var(--t-lg); margin:var(--s-10) 0 var(--s-3); }
.prose h3 { font-size:var(--t-base); margin:var(--s-6) 0 var(--s-2); }
.prose p,.prose li { color:var(--ink-2); }
.prose ul,.prose ol { padding-left:var(--s-6); margin:var(--s-3) 0; }
.prose li { margin:var(--s-2) 0; }

/* ==========================================================================
   AUTH SURFACE
   ========================================================================== */

.auth-wrap { min-height:100vh; display:grid; grid-template-columns:1fr 1fr; }

.auth-aside {
  background:linear-gradient(160deg,var(--brand-950),var(--brand-900) 45%,var(--brand-700));
  color:#fff; padding:var(--s-12); display:flex; flex-direction:column; justify-content:space-between;
}

/* Below this width the aside is dropped, so the grid must collapse to a single
   column too — otherwise the form keeps its half and the other half is dead space. */
@media (max-width:940px) {
  .auth-wrap { grid-template-columns:1fr; }
  .auth-aside { display:none; }
}
.auth-aside .brand-name,.auth-aside .brand { color:#fff; }
.auth-aside h2 { color:#fff; font-size:var(--t-2xl); max-width:18ch; margin-bottom:var(--s-4); }
.auth-aside p { color:rgba(255,255,255,.72); max-width:44ch; }
.auth-aside .points { list-style:none; padding:0; margin:var(--s-8) 0 0; display:grid; gap:var(--s-4); }
.auth-aside .points li {
  padding-left:var(--s-6); position:relative; color:rgba(255,255,255,.86); font-size:var(--t-sm);
}
.auth-aside .points li::before {
  content:"✓"; position:absolute; left:0; color:var(--ok); font-weight:700;
}
.auth-quote { border-top:1px solid rgba(255,255,255,.15); padding-top:var(--s-5); font-size:var(--t-sm); }

.auth-panel {
  display:flex; align-items:center; justify-content:center; padding:var(--s-8) var(--s-6);
  background:var(--bg-raised);
}
.auth-form { width:100%; max-width:404px; }
.auth-form h1 { font-size:var(--t-2xl); margin-bottom:var(--s-2); }
.auth-form .sub { color:var(--ink-3); margin-bottom:var(--s-8); font-size:var(--t-sm); }
.auth-form .foot { margin-top:var(--s-6); font-size:var(--t-sm); color:var(--ink-3); text-align:center; }

/* Hidden by default — the aside already brands the two-column layout. Must be
   declared after .auth-panel so the narrow-screen override below wins. */
.auth-brand-compact { display:none; margin-bottom:var(--s-8); }
@media (max-width:940px) {
  /* The aside carried the only branding; without it the form is anonymous. */
  .auth-brand-compact { display:inline-flex; }
  .auth-panel { align-items:flex-start; padding-top:var(--s-16); }
}
.auth-alt { margin-top:var(--s-5); padding-top:var(--s-5); border-top:1px solid var(--line-soft); }
.pw-meter { height:4px; border-radius:var(--r-full); background:var(--bg-sunken); overflow:hidden; }
.pw-meter > i { display:block; height:100%; width:0; transition:width .25s var(--ease), background .25s; }

/* ==========================================================================
   APP SURFACE
   ========================================================================== */

.app-shell { display:grid; grid-template-columns:var(--sidebar-w) 1fr; min-height:100vh; }
@media (max-width:1000px) { .app-shell { grid-template-columns:1fr; } }

.app-side {
  background:var(--bg-raised); border-right:1px solid var(--line);
  display:flex; flex-direction:column; position:sticky; top:0; height:100vh; overflow-y:auto;
}
@media (max-width:1000px) {
  .app-side {
    position:fixed; inset:0 auto 0 0; width:var(--sidebar-w); z-index:100;
    transform:translateX(-100%); transition:transform .22s var(--ease); box-shadow:var(--sh-5);
  }
  .app-side.open { transform:none; }
  .side-scrim { position:fixed; inset:0; background:rgba(8,12,20,.45); z-index:99; }
}
.app-side .side-head { padding:var(--s-5); border-bottom:1px solid var(--line-soft); }
.app-nav { padding:var(--s-4) var(--s-3); flex:1; display:flex; flex-direction:column; gap:var(--s-1); }
.app-nav .group {
  font-size:var(--t-xs); font-weight:700; letter-spacing:.1em; text-transform:uppercase;
  color:var(--ink-4); padding:var(--s-4) var(--s-3) var(--s-2);
}
.app-nav a {
  display:flex; align-items:center; gap:var(--s-3); padding:var(--s-2) var(--s-3);
  border-radius:var(--r-sm); font-size:var(--t-sm); font-weight:550; color:var(--ink-2);
  transition:background .12s var(--ease), color .12s var(--ease);
}
.app-nav a:hover { background:var(--bg-sunken); color:var(--ink); text-decoration:none; }
.app-nav a.on { background:var(--accent-soft); color:var(--accent-ink); font-weight:600; }
.app-nav a .ic { width:18px; height:18px; flex:0 0 auto; display:grid; place-items:center; }
.app-nav a .ic svg {
  width:18px; height:18px; display:block;
  fill:none; stroke:currentColor; stroke-width:1.6;
  stroke-linecap:round; stroke-linejoin:round;
  opacity:.75; transition:opacity .14s var(--ease);
}
.app-nav a:hover .ic svg { opacity:1; }
.app-nav a.on .ic svg { opacity:1; stroke-width:1.8; }
.app-nav a .count {
  margin-left:auto; font-size:var(--t-xs); font-weight:650; padding:1px var(--s-2);
  border-radius:var(--r-full); background:var(--bg-sunken); color:var(--ink-3);
}
.app-nav a.on .count { background:var(--bg-raised); color:var(--accent-ink); }
.side-foot { padding:var(--s-4); border-top:1px solid var(--line-soft); }

.app-main { display:flex; flex-direction:column; min-width:0; }
.app-top {
  height:var(--topbar-h); border-bottom:1px solid var(--line); background:var(--bg-raised);
  display:flex; align-items:center; justify-content:space-between; gap:var(--s-4);
  padding:0 var(--s-6); position:sticky; top:0; z-index:40;
}
/* `overflow-x:clip` rather than `hidden`: entrance animations slide content in
   from +32px, which on a phone is 32px past the right edge of a box that
   already fills the width — so the purchase funnel scrolled sideways for the
   length of its own animation. `clip` contains that without creating a scroll
   container, so the sticky table headers inside .table-wrap keep working. */
.app-content { padding:var(--s-8) var(--s-6) var(--s-16); max-width:1500px; width:100%; overflow-x:clip; }
.page-head { margin-bottom:var(--s-6); }
.page-head h1 { font-size:var(--t-2xl); margin-bottom:var(--s-2); }
.page-head p { color:var(--ink-3); margin:0; max-width:76ch; }
/* Header with primary actions on the right (report downloads, etc.). */
.page-head-split {
  display:flex; align-items:flex-start; justify-content:space-between;
  gap:var(--s-4); flex-wrap:wrap;
}
.page-head-split > :first-child { min-width:min(100%, 280px); flex:1 1 280px; }
.page-head-split .page-head-actions {
  display:flex; align-items:center; gap:var(--s-3); flex-wrap:wrap;
  flex:0 0 auto; padding-top:var(--s-1);
}
.page-head .eyebrow { margin-bottom:var(--s-2); }
/* For pages whose real content is a canvas (map, simulator). The standing
   explainer is worth keeping, but it must not cost the tool half a viewport. */
/* Workspace vertical rhythm: every top-level band is separated by --s-8,
   the same single value /app/data repeats between all of its sections.
   These four rules used to read 16 / 20 / 24 / 24 — four numbers, each set
   locally, none referring to a shared rhythm. `.page-head-compact`,
   `.scope-bar`, `.city-context` and `.ws-tabs` are used on the workspace and
   nowhere else, so this is scoped by construction. */
.page-head-compact { margin-bottom:var(--s-8); }
.page-head-compact h1 { font-size:var(--t-3xl); margin-bottom:var(--s-1); }
/* :not(.eyebrow) — the eyebrow is a <p class="eyebrow">, so this rule
   outranked `.eyebrow` on specificity (0,1,1 vs 0,1,0) and quietly rendered
   it at 13px against the design system's 11.5px. */
.page-head-compact p:not(.eyebrow) { font-size:var(--t-sm); max-width:70ch; }

.user-btn {
  display:flex; align-items:center; gap:var(--s-3); padding:var(--s-2); border-radius:var(--r);
  border:1px solid transparent; background:none; cursor:pointer; width:100%; text-align:left;
}
.user-btn:hover { background:var(--bg-sunken); }
.avatar {
  width:32px; height:32px; border-radius:50%; flex:0 0 auto; display:grid; place-items:center;
  background:var(--accent-soft); color:var(--accent-ink); font-weight:700; font-size:var(--t-xs);
}
.user-meta { min-width:0; display:flex; flex-direction:column; gap:1px; }
.user-meta .n {
  display:block; font-size:var(--t-sm); font-weight:600; color:var(--ink);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:150px;
}
.user-meta .e {
  display:block; font-size:var(--t-xs); color:var(--ink-3);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:150px;
}

/* Plan cards */
.plan-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(320px,1fr)); gap:var(--s-4); }
.dashboard-city-brief {
  position:relative; isolation:isolate; min-height:176px; overflow:hidden;
  display:flex; align-items:center; margin:0 0 var(--s-5); padding:var(--s-5);
  border:1px solid var(--line); border-radius:var(--r-md); background:var(--bg-raised); box-shadow:var(--sh-1);
}
.dashboard-city-brief::after {
  content:""; position:absolute; inset:0; z-index:-1; pointer-events:none;
  background:linear-gradient(90deg,var(--bg-raised) 0%,color-mix(in srgb,var(--bg-raised) 92%,transparent) 47%,color-mix(in srgb,var(--bg-raised) 18%,transparent) 100%);
}
.dashboard-city-brief > img {
  position:absolute; z-index:-2; inset:0 0 0 auto; width:58%; height:100%; display:block;
  object-fit:cover; object-position:center 52%; opacity:.94;
}
.dashboard-city-brief-copy { max-width:48ch; }
.dashboard-city-brief-copy .eyebrow { margin-bottom:var(--s-2); }
.dashboard-city-brief-copy h2 { margin-bottom:var(--s-2); font-size:var(--t-xl); }
.dashboard-city-brief-copy p:last-child { margin:0; color:var(--ink-3); font-size:var(--t-sm); }
.plan {
  background:var(--bg-raised); border:1px solid var(--line); border-radius:var(--r-md);
  padding:var(--s-5); box-shadow:var(--sh-1); display:flex; flex-direction:column; gap:var(--s-4);
}
.plan-city-art { margin:calc(var(--s-5) * -1) calc(var(--s-5) * -1) 0; height:144px; overflow:hidden; border-radius:var(--r-md) var(--r-md) 0 0; background:var(--accent-soft); }
.plan-city-art img { width:100%; height:100%; object-fit:cover; object-position:center; display:block; }
.plan-top { display:flex; align-items:flex-start; justify-content:space-between; gap:var(--s-3); }
/* Same card, two heading levels: h3 under the dashboard's "Recent plans",
   h2 on /app/plans where it hangs off the page h1. Pinned so promoting the
   tag for the outline does not resize the card. */
.plan-top h2,.plan-top h3 { font-size:var(--t-md); }
.plan .svc { font-size:var(--t-xs); font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--accent); }
.plan h3 { font-size:var(--t-md); margin-top:2px; }
.plan .scope-line { font-size:var(--t-sm); color:var(--ink-3); }
.plan-meta {
  display:grid; grid-template-columns:1fr 1fr; gap:var(--s-3);
  padding-top:var(--s-4); border-top:1px solid var(--line-soft); font-size:var(--t-xs);
}
.plan-meta .k { color:var(--ink-3); }
.plan-meta .v { color:var(--ink); font-weight:600; }
.plan-actions { display:flex; gap:var(--s-2); flex-wrap:wrap; }

@media (max-width:640px) {
  .dashboard-city-brief { min-height:204px; align-items:flex-end; padding:var(--s-4); }
  .dashboard-city-brief::after { background:linear-gradient(0deg,var(--bg-raised) 11%,color-mix(in srgb,var(--bg-raised) 87%,transparent) 58%,color-mix(in srgb,var(--bg-raised) 22%,transparent) 100%); }
  .dashboard-city-brief > img { inset:0; width:100%; object-position:center 45%; }
  .dashboard-city-brief-copy { max-width:36ch; }
}

/* Scope confirmation — the moment before money changes hands */
.scope-box {
  border:1.5px solid var(--accent); border-radius:var(--r-md); overflow:hidden;
  background:var(--bg-raised);
}
.scope-box .hd {
  background:var(--accent-soft); padding:var(--s-3) var(--s-5);
  font-size:var(--t-xs); font-weight:700; letter-spacing:.1em; text-transform:uppercase;
  color:var(--accent-ink);
}
.scope-box .bd { padding:var(--s-5); }
.scope-rows { display:grid; gap:var(--s-3); }
.scope-row {
  display:flex; justify-content:space-between; gap:var(--s-4); padding-bottom:var(--s-3);
  border-bottom:1px solid var(--line-soft); font-size:var(--t-sm);
}
.scope-row:last-child { border-bottom:0; padding-bottom:0; }
.scope-row .k { color:var(--ink-3); }
.scope-row .v { font-weight:640; color:var(--ink); text-align:right; }
.scope-row.total .v { font-size:var(--t-lg); }

/* Report progress */
.progress-list { display:grid; gap:var(--s-1); }
.progress-step {
  display:flex; align-items:center; gap:var(--s-3); padding:var(--s-3) var(--s-4);
  border-radius:var(--r); font-size:var(--t-sm); color:var(--ink-3);
}
.progress-step .mark {
  width:20px; height:20px; border-radius:50%; flex:0 0 auto; display:grid; place-items:center;
  border:1.5px solid var(--line); font-size:var(--t-xs); font-weight:700;
}
.progress-stage-art { width:32px; height:32px; flex:0 0 auto; opacity:.72; }
.progress-step.running .progress-stage-art { opacity:1; }
.progress-step.complete .progress-stage-art { opacity:.88; }
.progress-step.complete { color:var(--ink-2); }
.progress-step.complete .mark { background:var(--ok); border-color:var(--ok); color:#fff; }
.progress-step.running { background:var(--accent-soft); color:var(--accent-ink); font-weight:600; }
.progress-step.running .mark { border-color:var(--accent); border-top-color:transparent; animation:spin .8s linear infinite; }
.progress-step.failed .mark { background:var(--bad); border-color:var(--bad); color:#fff; }
.progress-step .detail { margin-left:auto; font-size:var(--t-xs); color:var(--ink-3); font-weight:400; }

/* Report viewer */
/* The report is the paid deliverable and runs to several thousand pixels. A
   fixed frame height turned it into a letterbox with two competing scrollbars —
   the page's and the iframe's. ui.js measures the content (same origin) and
   grows the frame to fit, so the page is the only thing that scrolls. The
   height below is the pre-measurement and no-JS fallback. */
.report-frame {
  display:block; width:100%; height:calc(100vh - 210px); min-height:560px;
  border:1px solid var(--line); border-radius:var(--r-md); background:var(--bg-raised);
}
.report-frame.is-autosized { height:auto; min-height:0; }
.panel-narrow { max-width:640px; }
.progress-track { height:var(--s-2); margin:var(--s-5) 0; }
.ws-embed {
  width:100%; height:780px; border:1px solid var(--line);
  border-radius:var(--r); background:var(--bg-raised);
}
.ws-module-foot {
  margin-top:var(--s-3); border-top:1px solid var(--line); padding-top:var(--s-3);
}

/* ==========================================================================
   WORKSPACE — entitlement-scoped analytics
   ========================================================================== */

.scope-bar {
  display:flex; align-items:center; gap:var(--s-4); flex-wrap:wrap;
  padding:var(--s-3) var(--s-5); border:1px solid var(--accent); border-radius:var(--r-md);
  background:var(--accent-soft); margin-bottom:var(--s-8); font-size:var(--t-sm);
}
/* The glyph lives in the markup, not a ::before. It used to be both — a ◈ from
   here and a 🔒 from the template, rendering two unrelated icons side by side. */
.scope-bar .lock {
  display:inline-flex; align-items:center; gap:var(--s-2);
  color:var(--accent-ink); font-weight:700;
}
.scope-bar .lock svg {
  width:15px; height:15px; flex:0 0 auto;
  fill:none; stroke:currentColor; stroke-width:1.7;
  stroke-linecap:round; stroke-linejoin:round;
}
.scope-bar .item { display:flex; align-items:baseline; gap:var(--s-2); }
.scope-bar .item .k { font-size:var(--t-xs); text-transform:uppercase; letter-spacing:.07em; color:var(--ink-3); }
.scope-bar .item .v { font-weight:650; color:var(--ink); }
.scope-bar .spacer { margin-left:auto; }

.ws-tabs {
  display:flex; gap:var(--s-1); border-bottom:1px solid var(--line);
  margin-bottom:var(--s-8); overflow-x:auto; -webkit-overflow-scrolling:touch;
}
.ws-tabs button {
  border:0; background:none; padding:var(--s-3) var(--s-4); font:550 var(--t-sm)/1 var(--font);
  color:var(--ink-3); cursor:pointer; border-bottom:2px solid transparent;
  white-space:nowrap; transition:color .12s var(--ease), border-color .12s var(--ease);
}
.ws-tabs button:hover { color:var(--ink); }
.ws-tabs button.on { color:var(--accent); border-bottom-color:var(--accent); }

/* --------------------------------------------------------------------------
   The spine of a workspace tab.
   --------------------------------------------------------------------------
   /app/data names every band it shows — "Dataset health", and under it one
   line saying what the band is for. Ten workspace tabs named none of theirs:
   a reader landed on a toolbar, pressed a button, and got cards with no
   statement of what they were looking at or why. The tabs are not short of
   explanation — each one carries a paragraph of it in the template comments
   and another in its empty state — it was simply never on screen once the
   panel had content.

   Three parts, in a fixed order, on every tab:

     .panel-section   what this tab answers, above its controls
     .toolbar         the controls (already shared)
     .run-summary     what the last run actually produced

   `.panel-section` is `.section-label-row` with the workspace's own rhythm:
   the row above a toolbar needs more air under it than the row above a card
   grid, because a --bg-sunken box follows it rather than white cards.
   -------------------------------------------------------------------------- */
.panel-section { margin-bottom:var(--s-5); }
.panel-section h2 { font-size:var(--t-lg); }
/* The heading and its purpose take the slack; the meta keeps its own width.
   The other way round, a three-line purpose squeezed the caption beside it to
   "Fit score…" — an ellipsis where the whole string is six words long. */
.panel-section > div:first-child { flex:1 1 auto; min-width:0; }
/* The right-hand slot: a scope reminder, a count, a source note. One line, on
   the heading's baseline — a caption that wraps lifts the heading beside it
   off its own line. Keep these short; the purpose sentence is the long one. */
.panel-section-meta {
  flex:0 0 auto; max-width:40%; text-align:right;
  color:var(--ink-3); font-size:var(--t-xs); white-space:nowrap;
  overflow:hidden; text-overflow:ellipsis;
}

/* What the last run produced, in one line, in one place.
 *
 * This was a `<span class="muted small">` that each tab dropped as the last
 * child of its own `.toolbar`. Being a flex item of the controls row, it
 * landed wherever the row had slack: beside the button on Proximity, alone on
 * a second line on Urban Fabric, and on H3 — where nothing wrote to it — as
 * nothing at all. Three placements and one absence for the same sentence.
 *
 * Out of the toolbar and into a band of its own, directly under it, so the
 * figures a run produced are always in the same place relative to the button
 * that produced them. Empty until a run writes to it, and it takes no space
 * while empty — an empty rule under a toolbar reads as a failed render. */
.run-summary {
  margin:0 0 var(--s-5);
  padding:var(--s-2) var(--s-4);
  border:1px solid var(--line-soft); border-radius:var(--r);
  background:var(--bg-sunken);
  color:var(--ink-2); font-size:var(--t-xs); line-height:1.5;
  font-variant-numeric:tabular-nums;
}
.run-summary:empty { display:none; }
/* The figures inside it are the point; the connective tissue is not. */
.run-summary b { color:var(--ink); font-weight:650; }
/* A toolbar with a summary under it owns the gap between the two — they are
   one unit, the controls and what the controls produced. Where `:has()` is
   unsupported the toolbar keeps its own --s-5 and the pair simply sits a step
   further apart; nothing misaligns. */
.toolbar:has(+ .run-summary) { margin-bottom:var(--s-3); }
/* Stacked tables that answer the same question with the same columns.
 *
 * Competitors renders up to four of them — direct rivals, same trade, anchors,
 * complementary — and an auto layout sized each to its own longest outlet name,
 * so the header lefts landed at 621, 881 and 764px in one card. Same columns,
 * same widths, whatever the rows say. */
/* Percentage columns need a floor: at 375px the 16% Type column resolves to
   46px and the "independent" chip in it is 96px, so the cell overflowed. Below
   the floor the table scrolls inside its .table-wrap and the columns keep the
   widths that make the four groups line up. */
/* The floor is set by the widest fixed cell, not by taste: the Type column
   carries an "independent" chip that measures 96px, so at 24% the column needs
   600px of table before the chip stops overflowing its cell padding. */
.table-cols-outlet { table-layout:fixed; min-width:600px; }
.table-cols-outlet th:nth-child(1),.table-cols-outlet td:nth-child(1) { width:30%; }
.table-cols-outlet th:nth-child(2),.table-cols-outlet td:nth-child(2) { width:22%; }
.table-cols-outlet th:nth-child(3),.table-cols-outlet td:nth-child(3) { width:11%; }
.table-cols-outlet th:nth-child(4),.table-cols-outlet td:nth-child(4) { width:13%; }
.table-cols-outlet th:nth-child(5),.table-cols-outlet td:nth-child(5) { width:24%; }
.table-cols-outlet td:first-child { overflow-wrap:anywhere; }

/* ---------------------------------------------------- the format finder */
/* FitFinder's catalogue search. Positioned relative so the result list can
   overlay the tab panel below it rather than pushing the whole workspace down
   every time a customer types — the list is transient, the layout is not. */
/* The franchise picker, and the catalogue search that lives inside it.
 *
 * FitFinder's search used to be one bar above the tab bar. It reached the whole
 * catalogue but it was not where the question is asked: a customer reading the
 * Franchise field in Urban Fabric had to scroll past the tabs to change it. The
 * search is part of the field now, so every tab that asks for a franchise also
 * offers the 17,000 formats behind it — see partials/format_picker.html.
 *
 * The results list is positioned against the field, so the field establishes
 * the containing block and has to out-stack its neighbours while open. */
.format-picker { position:relative; }
/* The catalogue picker carries a search box above its select and a result
   count beside its label, so it needs two of the toolbar's columns. */
.toolbar .format-picker { grid-column:span 2; }
@media (max-width:640px) { .toolbar .format-picker { grid-column:auto; } }
/* Only the select is pinned to the bottom of a toolbar row; the search sits
   above it and takes whatever slack the row has, so every picker's select
   still lines up with the plain fields beside it. */
.toolbar .format-picker > .ff-search { margin-top:auto; }
.format-picker-label {
  display:flex; align-items:baseline; justify-content:space-between; gap:var(--s-3);
}
.ff-status {
  font-weight:500; text-transform:none; letter-spacing:0;
  font-size:var(--t-xs); color:var(--ink-3); text-align:right;
  /* A count that wraps lifts its own label a line clear of the labels beside
     it. It is a caption, so it truncates rather than reflows the row. */
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0;
}
.ff-status:empty { display:none; }
.ff-results {
  position:absolute; z-index:600; top:100%; left:0; right:0;
  margin-top:4px; max-height:340px; overflow-y:auto;
  background:var(--bg-raised); border:1px solid var(--line);
  border-radius:var(--r-md); box-shadow:var(--sh-4);
}
.ff-row {
  display:flex; flex-direction:column; gap:2px; width:100%; text-align:left;
  padding:9px 12px; border:0; border-bottom:1px solid var(--line);
  background:none; cursor:pointer; font:inherit;
}
.ff-row:last-child { border-bottom:0; }
.ff-row:hover, .ff-row:focus-visible { background:var(--accent-soft); }
.ff-name { font-weight:600; color:var(--ink); font-size:var(--t-sm); }
.ff-meta { color:var(--ink-3); font-size:var(--t-xs); }
.ff-empty { margin:0; padding:var(--s-3); font-size:var(--t-sm); }

/* The controls band that opens every workspace tab.
 *
 * It was the only unboxed row in the stack, so three things that read as one
 * column started at three different left edges: the toolbar's first control at
 * the content edge, the format finder's input 17px in from its own padding, and
 * the card content below it 21px in. Boxed like the finder and padded to the
 * card's --s-5, all three now share one edge.
 *
 * Fields carried no flex basis, so each one was as wide as its own label —
 * "Locality" rendered a 150px select next to a 206px number input for no reason
 * a reader could see. A shared basis makes the row a grid again. */
/* A grid, not a wrapping flex row.
 *
 * As flex the controls were as wide as their own flex-basis and the row broke
 * wherever it ran out of width: Proximity's five fields filled the first line
 * and dropped "Build graph" onto a second one, left-aligned under "Locality",
 * where the primary action of the tab read as a stray button. The same row on
 * the H3 map kept its button inline. Two tabs, two shapes, from one rule.
 *
 * A grid puts every control in a column of the same width on every tab, so the
 * labels share one ceiling, the controls share one floor, and a row that has
 * to wrap wraps onto the same columns rather than a new arrangement.
 *
 * The 200px ceiling is measured, not chosen: it is the widest column that
 * still fits five of them across the 1116px content width, and five is what
 * the busiest toolbars need to keep their action on the same row as their
 * controls — the format picker takes two columns, so Urban Fabric is 1+2+1+1
 * and Proximity's first row is 1+2+1+1. At 230px only four fit and the button
 * started a row of its own. `justify-content:start` packs the columns left
 * rather than spreading the slack between them. */
.toolbar {
  display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,200px));
  justify-content:start; align-items:end; gap:var(--s-4);
  margin-bottom:var(--s-5); padding:var(--s-5);
  border:1px solid var(--line); border-radius:var(--r-md);
  background:var(--bg-sunken);
}
/* For a control whose options are sentences — "Who is within a reach radius"
   truncates in one column at any type size worth reading. */
.toolbar .field-wide { grid-column:span 2; }
@media (max-width:640px) {
  .toolbar { grid-template-columns:minmax(0,1fr); }
  .toolbar .field-wide { grid-column:auto; }
}
/* Already inside a surface, and already on the card's left edge. */
.card-body > .toolbar {
  padding:0; border:0; background:none; border-radius:0; margin-bottom:var(--s-4);
}
/* A controls row that follows content inside a card owes that content the
   same gap it leaves under itself, and a row that closes a card owes nothing
   — the card body's own padding is already there. */
.card-body > * + .toolbar { margin-top:var(--s-4); }
.card-body > .toolbar:last-child { margin-bottom:0; }
/* A grid item that is allowed to be narrower than its content; without this a
   long `<option>` sets the column's minimum and the row overflows the card. */
.toolbar .field, .toolbar > * { min-width:0; }
/* Labels along the top of the row, controls along its bottom. A label that
   wraps to two lines — "Budget (₹ lakh) · from your plan", which the engine
   appends at runtime — used to push its own caption a line clear of every
   caption beside it. Stretching the fields and pinning the controls down keeps
   both ends of the row straight whatever the labels do. */
/* The toolbar is the one --bg-sunken surface these labels appear on, and
   --ink-3 — which clears AA on --bg-raised — measures 4.15:1 there at 11.5px.
   One step darker, on this surface only. */
.toolbar .field > .label { min-height:1.2em; color:var(--ink-2); }
.toolbar .field { display:flex; flex-direction:column; }
.toolbar .field > input,
.toolbar .field > select,
.toolbar .field > textarea,
.toolbar .field > .row { margin-top:auto; }
/* Buttons and status text keep their own width inside their column rather
   than stretching across it — a 230px "Ask" is not a wider affordance, it is
   a wider target for the same word. */
.toolbar > :not(.field) { align-self:end; justify-self:start; }

.map-canvas-wrap { position:relative; }

/* Empty state for the map canvas, shown before a heatmap is rendered. Leaflet
   adds .leaflet-container to #map when it initialises, which is the signal to
   drop this — no JS coordination required. */
.map-empty {
  position:absolute; inset:0; z-index:400; pointer-events:none;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:var(--s-2); padding:var(--s-8); text-align:center; color:var(--ink-3);
}
.map-empty svg {
  width:46px; height:40px; margin-bottom:var(--s-2); color:var(--ink-4);
  fill:none; stroke:currentColor; stroke-width:1.3; stroke-linejoin:round;
}
.map-empty p { margin:0; max-width:44ch; font-size:var(--t-sm); }
.map-empty strong { color:var(--ink-2); }
#map.leaflet-container ~ .map-empty,
#site-map.leaflet-container ~ .map-empty,
#prox-map.leaflet-container ~ .map-empty,
#catch-map.leaflet-container ~ .map-empty,
#morph-map.leaflet-container ~ .map-empty { display:none; }
/* The map is the primary interface on this tab, but ~540px of page chrome sits
   above it. A fixed 620px meant that on a 900px-tall laptop only a third of the
   map was ever on screen. Take whatever the viewport has left instead, with a
   floor so a short window still gets a usable canvas. */
#map,#site-map,#catch-map,#prox-map,#morph-map {
  height:clamp(460px, calc(100vh - 300px), 760px);
  border-radius:var(--r-md); border:1px solid var(--line); background:var(--bg-sunken);
}
/* Map plus a docked panel beside it. `.map-wrap` was used by the catchment tab
   but never defined, so its panel simply stacked under the map; the H3 tab
   instead floated its panel over the map as a 275px translucent overlay. Cell
   detail is a heading, a KPI row, a six-row table, a percentile list and an
   action — none of which fits 275px, and all of which covered the hexagons the
   reader was comparing it against. One layout now serves both tabs. */
/* `align-items:start` left the inspector at its content height — 173px of card
   beside a 600px map, and a 427px hole under it. The row is two panels, so it
   gets the two-panel gap and both panels get the row's height; the inspector
   scrolls its own overflow rather than setting the row's height. */
.map-wrap {
  display:grid; gap:var(--s-5); align-items:stretch;
  grid-template-columns:minmax(0,1fr) clamp(300px, 27%, 380px);
}
/* Positioning context for the legend overlay, and nothing else. */
.map-canvas-wrap { position:relative; min-width:0; }
.map-side {
  background:var(--bg-raised); border:1px solid var(--line); border-radius:var(--r-md);
  padding:var(--s-4); max-height:clamp(460px, calc(100vh - 300px), 760px);
  box-shadow:var(--sh-1);
}
/* The docked inspector, as one shape on every map tab.
 *
 * Four tabs had four: H3 a head plus a scrolling div, Proximity the same head
 * over different furniture, Urban Fabric the same again, and the catchment no
 * head at all — just a bare `.awaiting` that the renderer replaced with three
 * nested cards. The legend was a fifth thing, floating over the map in a
 * translucent plate on three of them and missing from the fourth.
 *
 * One contract now, in three rows:
 *
 *   .map-inspector-head   what the panel is, and its current subject
 *   .map-side-body        the answer — the only part that scrolls
 *   .legend               the key to the map, always in view
 *
 * A column rather than one scrolling box, so the head stays put and the key
 * does not scroll away from the map it explains. `min-height:0` on the row
 * and on the body is what lets the body — and only the body — be shorter than
 * its content: without it Proximity's 1800px of stats set the grid row height
 * and dragged the map to 1800px with it. */
.map-side {
  min-height:0; display:flex; flex-direction:column; gap:var(--s-3); overflow:hidden;
}
.map-side > .map-inspector-head,
.map-side > .legend { flex:0 0 auto; }
.map-side > .map-side-body {
  flex:1 1 auto; min-height:0; overflow-y:auto;
  /* The body is the scroll container, so it owns the gutter its scrollbar
     needs; without this the content shifted 15px left the moment a run
     produced enough rows to scroll. */
  scrollbar-gutter:stable;
}
/* The empty state, centred in the column rather than stranded at the top of
   it. It used to sit as a 96px box under the head with 621px of white beneath
   — a rail that reads as a failed render rather than as one waiting for a
   click. Centring rather than stretching: a 600px dashed rectangle is a
   heavier promise than "an answer goes here". */
.map-side > .map-side-body:has(> .awaiting:only-child) { display:grid; align-content:center; }
/* The key, docked under the answer rather than floating over the data.
 *
 * It used to be an absolutely-positioned plate in the map's bottom-left
 * corner, which is where a city's densest hexagons are as often as not — so
 * the one panel that exists to explain the picture was sitting on top of it.
 * Worse, it painted whether or not it had rows: a tab with no legend yet drew
 * a bare 14x30px white blob on the map, which reads as a rendering artefact.
 * Docked here it covers nothing, disappears when it has nothing to say, and
 * fills the bottom of a rail that was empty anyway. */
.map-side > .legend {
  margin:0; padding:var(--s-3) 0 0;
  border-top:1px solid var(--line-soft); border-bottom:0;
}
.map-side > .legend:empty { display:none; }
/* `.map-side-overlay` was the floating plate the key used to sit in. Every one
   of its four call sites is now a `.legend` in the row above, so the class,
   its blur, its z-index and the `:has()` guard that stopped it painting an
   empty white blob over the map are all gone with them. */
@media (max-width:1100px) {
  .map-wrap { grid-template-columns:minmax(0,1fr); }
  .map-side { max-height:none; }
}
.legend {
  display:flex; flex-direction:column; gap:var(--s-1); margin-bottom:var(--s-4);
  padding-bottom:var(--s-4); border-bottom:1px solid var(--line-soft);
}
.legend .row { display:flex; align-items:center; gap:var(--s-2); font-size:var(--t-xs); }
.legend .sw { width:16px; height:12px; border-radius:3px; flex:0 0 auto; }
.map-inspector-head {
  display:flex; align-items:baseline; justify-content:space-between; gap:var(--s-3);
  margin-bottom:var(--s-3); padding-bottom:var(--s-3); border-bottom:1px solid var(--line-soft);
}
.map-inspector-head b { font-size:var(--t-xs); font-weight:700; color:var(--ink-2);
  text-transform:uppercase; letter-spacing:.04em; }
/* The docked inspectors are a ~300px track, and their contents were written for
   a full-width card: long locality and shop names, cluster labels, and the
   "N observations · M sources" cell all pushed past the panel — `td.num` is
   nowrap globally, and an auto table sizes to its widest cell. Wrap, rather
   than let three panels each grow their own horizontal scrollbar. */
.map-side { overflow-wrap:anywhere; }
.map-side table { table-layout:fixed; }
.map-side td.num,.map-side th.num { white-space:normal; }
/* `.map-side .kpi-row`, `.map-side .kpi .l` and `.map-side .kpi .v` were
   defined here to stop the H3 inspector's two tiles disagreeing on label
   height and value size. The inspector renders `.metric-card` now, like the
   other three rails, so no `.kpi` reaches a rail at all and the overrides had
   nothing left to override. `.map-side .metric-card .m-head` above is the
   equivalent floor for the component that did survive. */
#map-detail { font-size:var(--t-sm); }
/* A brief tint when a click replaces the panel's contents: the panel is beside
   the map rather than under the cursor, so the change needs to announce itself. */
@keyframes map-detail-in { from { background:var(--accent-soft); } to { background:transparent; } }
#map-detail.updated { animation:map-detail-in .5s ease-out; border-radius:var(--r-sm); }
@media (prefers-reduced-motion:reduce) { #map-detail.updated { animation:none; } }

.map-disclosure { border:1px solid var(--line); border-radius:var(--r-md); background:var(--bg-raised); margin-bottom:var(--s-4); box-shadow:var(--sh-1); }
.map-disclosure > summary { display:flex; align-items:center; justify-content:space-between; gap:var(--s-4); padding:var(--s-4) var(--s-5); cursor:pointer; list-style:none; }
.map-disclosure > summary::-webkit-details-marker,.map-subdisclosure > summary::-webkit-details-marker { display:none; }
.map-disclosure > summary > span:first-child { display:flex; flex-direction:column; gap:3px; }
.map-disclosure > summary b { display:block; color:var(--ink); font-size:var(--t-sm); }
.map-disclosure > summary small { display:block; color:var(--ink-3); font-size:var(--t-xs); }
.map-disclosure-icon::before { content:"+"; color:var(--accent); font-size:var(--t-xl); font-weight:400; }
.map-disclosure[open] .map-disclosure-icon::before { content:"−"; }
.map-disclosure-body { padding:0 var(--s-5) var(--s-5); border-top:1px solid var(--line-soft); }
.map-layer-section { padding-top:var(--s-4); }
.section-inline-head { display:flex; align-items:baseline; justify-content:space-between; gap:var(--s-3); margin-bottom:var(--s-3); }
.section-inline-head h3 { margin:0; font-size:var(--t-sm); }
/* Network trading context.
   Everything here renders inside the ~300px docked inspector, where the base
   table's 12px cell padding wraps a three-column row onto two lines and a
   plain <ul> indents past the panel's own gutter. */
.band-table { font-size:var(--t-xs); table-layout:fixed; }
.band-table th, .band-table td { padding:var(--s-2) var(--s-1); }
.band-table th { color:var(--ink-3); font-weight:600; }
/* The three band columns are two digits wide; everything left over belongs to
   the role label, which is the only cell that may wrap. */
.band-table th.num, .band-table td.num { width:2.6rem; }
.band-table td:first-child { color:var(--ink-2); }
.route-list { list-style:none; margin:0 0 var(--s-3); padding:0; display:grid; gap:var(--s-2); }
.route-list li {
  display:flex; flex-wrap:wrap; align-items:baseline; gap:var(--s-2);
  font-size:var(--t-xs); padding-bottom:var(--s-2); border-bottom:1px solid var(--line-soft);
}
.route-list li:last-child { border-bottom:0; padding-bottom:0; }
.route-name { font-weight:650; color:var(--ink); font-size:var(--t-sm); }
/* The disclosure that sits under every graph surface. Quiet by default — it
   states a limit, it is not a warning — and closed, because a customer who
   wants the caveat looks for it and one who does not should still see it. */
.prove-not { margin-top:var(--s-4); border-top:1px solid var(--line-soft); padding-top:var(--s-3); }
.prove-not > summary { cursor:pointer; font-size:var(--t-xs); color:var(--ink-3); font-weight:600; }
.prove-not p { margin:var(--s-2) 0 0; font-size:var(--t-xs); color:var(--ink-3); line-height:1.55; }
/* Leaflet's tooltip, restyled as a flat map label. Without this an anchor name
   arrives as a white callout with a pointer arrow, which on a route map reads
   as a second kind of marker.

   Themed tokens, not a hardcoded white: the workspace has a dark mode, and a
   fixed white plate under --ink-2 renders pale text on white — a label the
   customer can see and cannot read. */
.leaflet-tooltip.anchor-label {
  background:var(--bg-raised); border:1px solid var(--line); box-shadow:var(--sh-1);
  color:var(--ink-2); font:650 var(--t-xs)/1.2 var(--font); padding:2px var(--s-2);
  border-radius:var(--r-sm); white-space:nowrap; opacity:.95;
}
.leaflet-tooltip.anchor-label::before { display:none; }

/* Frontage & site readiness.
   Two columns of very different shape — a six-row scorecard beside a
   nine-field property form — so this is its own grid rather than .grid-2,
   which would size both halves the same and leave the form cramped. */
.readiness-grid { display:grid; grid-template-columns:minmax(0,1.1fr) minmax(0,1fr); gap:var(--s-6); align-items:start; }
@media (max-width:900px) { .readiness-grid { grid-template-columns:minmax(0,1fr); } }

.readiness-list { list-style:none; margin:0; padding:0; display:grid; gap:var(--s-3); }
.readiness-item {
  padding:var(--s-3) var(--s-4); border:1px solid var(--line-soft);
  border-left:3px solid var(--line); border-radius:var(--r-sm); background:var(--bg);
}
/* The left edge carries the state, so the two rows still needing an answer are
   findable without reading a badge on every row. */
.readiness-item.r-unknown { border-left-color:var(--warn); }
.readiness-item.r-inferred,
.readiness-item.r-provided { border-left-color:var(--accent); }
.readiness-item.r-verified { border-left-color:var(--ok); }
.readiness-head { display:flex; align-items:center; justify-content:space-between; gap:var(--s-3); }
.readiness-label { font-size:var(--t-sm); font-weight:650; color:var(--ink); }
.readiness-finding { margin:var(--s-2) 0 0; font-size:var(--t-sm); color:var(--ink-2); line-height:1.5; }
.readiness-action { margin:var(--s-2) 0 0; font-size:var(--t-xs); color:var(--ink-3); line-height:1.5; }
.readiness-action span {
  display:inline-block; margin-right:var(--s-2); font-weight:700; color:var(--accent-ink);
  text-transform:uppercase; letter-spacing:.045em;
}

.readiness-form { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:var(--s-4); }
.readiness-wide { grid-column:1 / -1; }
.readiness-actions { display:flex; align-items:center; gap:var(--s-4); flex-wrap:wrap; }

.map-subdisclosure { margin-top:var(--s-4); border-top:1px solid var(--line-soft); }
.map-subdisclosure > summary { display:flex; align-items:baseline; justify-content:space-between; gap:var(--s-3); padding:var(--s-4) 0; cursor:pointer; color:var(--ink-2); font-size:var(--t-sm); font-weight:650; }
.map-subdisclosure > summary span { color:var(--ink-3); font-size:var(--t-xs); font-weight:400; }
.map-subdisclosure-body { padding-bottom:var(--s-2); }
@media (max-width:640px) { #map,#site-map,#catch-map,#prox-map,#morph-map { height:520px; } }

.pick-grid {
  display:grid; grid-template-columns:repeat(auto-fill,minmax(170px,1fr)); gap:var(--s-2);
  max-height:230px; overflow-y:auto; padding:2px;
}
.pick {
  display:flex; align-items:center; gap:var(--s-2); padding:var(--s-2) var(--s-3);
  font-size:var(--t-sm); border:1px solid var(--line); border-radius:var(--r-sm);
  cursor:pointer; background:var(--bg-raised); color:var(--ink-2);
}
.pick:hover { border-color:var(--accent); }
.pick.on { border-color:var(--accent); background:var(--accent-soft); font-weight:600; color:var(--accent-ink); }
.pick .sub { margin-left:auto; font-size:var(--t-xs); color:var(--ink-3); font-weight:400; }

/* `grid-auto-rows:1fr` for the same reason `.metric-grid` carries it: without
   it each row sizes to its own longest description, so a grid of sixty cards
   steps up and down the page and the badge at the foot of each one sits at a
   different height from the badge beside it. */
.card-grid {
  display:grid; grid-template-columns:repeat(auto-fill,minmax(255px,1fr));
  grid-auto-rows:1fr; gap:var(--s-4);
}
.fcard {
  display:flex; flex-direction:column; min-width:0;
  background:var(--bg-raised); border:1px solid var(--line); border-radius:var(--r-md);
  padding:var(--s-4); transition:border-color .14s var(--ease), transform .14s var(--ease);
}
/* The pointer and the lift belong only to cards that do something. A plan is
   locked to one franchise, so most of this grid is a catalogue to read, not a
   control to press. */
.fcard.is-actionable { cursor:pointer; }
.fcard.is-actionable:hover { border-color:var(--accent); transform:translateY(-1px); }
/* The category, with the head to itself.
   It used to share the row with the provenance badge, which is a different
   length on every card and left the category about 120px of a 255px card — so
   "Car Wash / Ceramic Coating / Detailing" rendered as "CAR WASH / CE…", which
   removes the whole of what a category label is for. The badge is pinned to
   the card's foot instead, exactly as `.metric-card .m-badge` is, so a row of
   badges lines up whatever the cards above them hold. */
/* Two lines, with a floor, rather than one line and an ellipsis. The
   catalogue's real labels run to "Car Wash / Ceramic Coating / Detailing" and
   "Car Maintenance & Repair Services"; at a 255px card neither fits on one
   line at any size worth reading. The floor is in em of the label's own line,
   so a one-line and a two-line category still put the brand names beneath
   them on the same baseline across a row. */
.fcard-head {
  display:flex; align-items:flex-start; gap:var(--s-2);
  min-height:2.6em; margin-bottom:2px;
}
.fcard-head .cat {
  min-width:0; overflow:hidden; line-height:1.25;
  display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; line-clamp:2;
}
.fcard-badge { margin-top:auto; padding-top:var(--s-3); }
.fcard-badge:empty { display:none; }
.fcard-badge .badge { max-width:100%; white-space:normal; }
.fcard h4 { margin-bottom:2px; font-size:var(--t-base); }
.fcard .cat {
  font-size:var(--t-xs); color:var(--accent); text-transform:uppercase;
  letter-spacing:.05em; font-weight:650;
}
.fcard .desc { font-size:var(--t-xs); color:var(--ink-3); margin:var(--s-2) 0; min-height:32px; }
.fcard dl { display:grid; grid-template-columns:auto auto; gap:2px var(--s-3); margin:0; font-size:var(--t-xs); }
.fcard dt { color:var(--ink-3); }
.fcard dd { margin:0; text-align:right; font-variant-numeric:tabular-nums; color:var(--ink-2); }

.result {
  border:1px solid var(--line); border-radius:var(--r-md); padding:var(--s-4);
  margin-bottom:var(--s-3); background:var(--bg-raised);
}
.result:hover { border-color:var(--accent); }
.result .head { display:flex; align-items:baseline; gap:var(--s-3); flex-wrap:wrap; }
.result .head h4 { font-size:var(--t-base); }
.result .head .rank { font-size:var(--t-xs); color:var(--ink-3); font-weight:650; }
.result .summary { font-size:var(--t-sm); margin:var(--s-2) 0; }
.result .metrics { display:flex; gap:var(--s-4); flex-wrap:wrap; font-size:var(--t-xs); color:var(--ink-3); }
.result .metrics b { color:var(--ink); font-variant-numeric:tabular-nums; }
.result .drivers { margin-top:var(--s-2); font-size:var(--t-xs); }

.derived {
  margin-top:var(--s-4); padding:var(--s-4); border-radius:var(--r);
  background:var(--bg-sunken); border:1px solid var(--line);
}
.derived h4 {
  margin-bottom:var(--s-2); font-size:var(--t-xs); text-transform:uppercase;
  letter-spacing:.06em; color:var(--ink-3);
}
.derived dl {
  display:grid; grid-template-columns:repeat(auto-fill,minmax(190px,1fr));
  gap:var(--s-1) var(--s-4); margin:0; font-size:var(--t-sm);
}
.derived .row {
  display:flex; justify-content:space-between; gap:var(--s-2);
  border-bottom:1px dotted var(--line); padding-bottom:3px;
}
.derived .row b { font-variant-numeric:tabular-nums; }

/* The question box.
 *
 * It was a full-width slab of brand gradient at the top of the first tab —
 * the most saturated thing in the product, sitting above the numbers a
 * customer paid for and out-shouting every one of them. Colour in this
 * product means fit; a control that means nothing on that scale should not be
 * the loudest thing on the page.
 *
 * The reference page's hero treatment instead: a raised card with one soft
 * radial accent in the corner, ink text, and the accent kept for the button
 * that actually does something. It is still the first thing on the tab and
 * still obviously a place to type, and now the KPI row below it reads as the
 * more important object, which it is.
 *
 * Padding is --s-5, not --s-6: this box sits directly above a column of cards
 * and its content has to start on their left edge. At 24px it started 3px
 * right of every card on the page, which is visible down a 1156px column. */
.ask-box {
  background:
    radial-gradient(circle at 96% 6%, var(--accent-soft), transparent 42%),
    var(--bg-raised);
  border:1px solid var(--line); border-radius:var(--r-md); box-shadow:var(--sh-1);
  padding:var(--s-5); margin-bottom:var(--s-5); color:var(--ink);
}
.ask-box h2 { font-size:var(--t-md); margin-bottom:var(--s-1); }
.ask-box .ask-lede { color:var(--ink-3); font-size:var(--t-sm); margin:0 0 var(--s-4); }
.ask-row { display:flex; gap:var(--s-3); }
.ask-row input { flex:1; height:var(--control-h-lg); }
.ask-row .btn { height:var(--control-h-lg); flex:0 0 auto; }
/* Suggested questions, as the quiet chips they are. They were white-on-brand
   pills that read as the box's primary actions; they are examples. */
.ask-chips { display:flex; gap:var(--s-2); flex-wrap:wrap; margin-top:var(--s-3); }
.ask-chips span {
  font-size:var(--t-xs); padding:5px var(--s-3); border-radius:var(--r-full); cursor:pointer;
  background:var(--bg-sunken); border:1px solid var(--line-soft); color:var(--ink-2);
  transition:border-color .12s var(--ease), color .12s var(--ease);
}
.ask-chips span:hover { border-color:var(--accent); color:var(--accent-ink); }
/* How the question was read back. It was tinted with rgba(0,0,0,.18), which is
   a colour that only exists on a dark ground — on the raised card it is a grey
   smear. A callout, like every other "here is what that means" in the app. */
.interpretation {
  margin-top:var(--s-4); padding:var(--s-3) var(--s-4);
  border:1px solid var(--line-soft); border-left:3px solid var(--accent);
  border-radius:0 var(--r) var(--r) 0;
  background:var(--bg-sunken); color:var(--ink-2); font-size:var(--t-sm);
}

.sparkline { width:100%; height:190px; }
.tornado-row {
  display:grid; grid-template-columns:150px 1fr; gap:var(--s-3);
  align-items:center; margin-bottom:var(--s-2); font-size:var(--t-xs);
}
.tornado-bar { position:relative; height:20px; background:var(--bg-sunken); border-radius:var(--r-sm); }
.tornado-bar > i { position:absolute; top:0; height:100%; border-radius:var(--r-sm); }
.tornado-bar .mid { position:absolute; left:50%; top:0; bottom:0; width:1px; background:var(--ink-3); opacity:.4; }

.idchip {
  display:inline-block; margin-left:var(--s-2); padding:1px var(--s-2); border-radius:var(--r-sm);
  background:var(--bg-sunken); color:var(--ink-3); font-family:var(--mono);
  font-size:var(--t-xs); cursor:help;
}

.scroll-y { max-height:380px; overflow-y:auto; }
.scroll-x { overflow-x:auto; }

/* ==========================================================================
   ADMIN
   ========================================================================== */

.admin-stats { display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:var(--s-3); }
.recon-item {
  display:flex; align-items:center; justify-content:space-between; gap:var(--s-4);
  padding:var(--s-3) var(--s-4); border-radius:var(--r); border:1px solid var(--line);
  font-size:var(--t-sm); background:var(--bg-raised);
}
.recon-item.bad { border-color:var(--bad); background:var(--bad-soft); }
.recon-item .v { font-weight:700; font-variant-numeric:tabular-nums; }

/* ==========================================================================
   Print
   ========================================================================== */

@media print {
  .app-side,.app-top,.pub-nav,.pub-footer,#toast-host,.ws-tabs,.toolbar { display:none !important; }
  .app-shell { grid-template-columns:1fr; }
  body { background:#fff; }
}

/* ==========================================================================
   ONBOARDING REDESIGN — Animation keyframes
   ========================================================================== */

@keyframes fadeInUp {
  from { opacity:0; transform:translateY(24px); }
  to   { opacity:1; transform:translateY(0); }
}
@keyframes fadeInDown {
  from { opacity:0; transform:translateY(-16px); }
  to   { opacity:1; transform:translateY(0); }
}
@keyframes scaleIn {
  from { opacity:0; transform:scale(.92); }
  to   { opacity:1; transform:scale(1); }
}
@keyframes slideInRight {
  from { opacity:0; transform:translateX(32px); }
  to   { opacity:1; transform:translateX(0); }
}
@keyframes pulseGlow {
  0%,100% { box-shadow:0 0 0 0 rgba(47,109,246,.25); }
  50%     { box-shadow:0 0 0 8px rgba(47,109,246,0); }
}
@keyframes floatUp {
  0%,100% { transform:translateY(0) rotate(0deg); }
  50%     { transform:translateY(-14px) rotate(3deg); }
}
@keyframes floatDown {
  0%,100% { transform:translateY(0) rotate(0deg); }
  50%     { transform:translateY(10px) rotate(-2deg); }
}
@keyframes gradientShift {
  0%   { background-position:0% 50%; }
  50%  { background-position:100% 50%; }
  100% { background-position:0% 50%; }
}
@keyframes drawLine {
  from { width:0; }
  to   { width:100%; }
}
@keyframes checkPop {
  0%   { transform:scale(1); }
  40%  { transform:scale(1.25); }
  100% { transform:scale(1); }
}

.anim-fade-up   { animation:fadeInUp .5s var(--ease) both; }
.anim-fade-down { animation:fadeInDown .4s var(--ease) both; }
.anim-scale     { animation:scaleIn .4s var(--ease) both; }
.anim-slide-r   { animation:slideInRight .5s var(--ease) both; }

.anim-d1 { animation-delay:.05s; }
.anim-d2 { animation-delay:.12s; }
.anim-d3 { animation-delay:.2s; }
.anim-d4 { animation-delay:.28s; }
.anim-d5 { animation-delay:.36s; }
.anim-d6 { animation-delay:.44s; }

/* ==========================================================================
   ONBOARDING — Glassmorphism utilities
   ========================================================================== */

.glass {
  background:color-mix(in srgb, var(--bg-raised) 72%, transparent);
  backdrop-filter:saturate(180%) blur(16px);
  -webkit-backdrop-filter:saturate(180%) blur(16px);
  border:1px solid color-mix(in srgb, var(--line) 50%, transparent);
}
.glass-strong {
  background:color-mix(in srgb, var(--bg-raised) 85%, transparent);
  backdrop-filter:saturate(200%) blur(24px);
  -webkit-backdrop-filter:saturate(200%) blur(24px);
  border:1px solid color-mix(in srgb, var(--line) 65%, transparent);
}

/* ==========================================================================
   ONBOARDING — Step indicator
   ========================================================================== */

.step-indicator {
  display:flex; align-items:center; gap:0;
  margin-bottom:var(--s-8); padding:var(--s-4) var(--s-5);
  background:var(--bg-raised); border:1px solid var(--line);
  border-radius:var(--r-lg); box-shadow:var(--sh-1);
}
.step-indicator .si-step {
  display:flex; align-items:center; gap:var(--s-3);
  padding:var(--s-2) 0; white-space:nowrap;
}
.step-indicator .si-num {
  width:32px; height:32px; border-radius:50%; flex:0 0 auto;
  display:grid; place-items:center; font-weight:700; font-size:var(--t-sm);
  border:2px solid var(--line); color:var(--ink-4); background:var(--bg);
  transition:all .3s var(--ease);
}
.step-indicator .si-label {
  font-size:var(--t-sm); font-weight:550; color:var(--ink-4);
  transition:color .3s var(--ease);
}
.step-indicator .si-line {
  flex:1; height:2px; background:var(--line); margin:0 var(--s-3);
  position:relative; min-width:24px;
}
.step-indicator .si-line::after {
  content:""; position:absolute; left:0; top:0; height:100%;
  background:var(--accent); width:0; border-radius:2px;
  transition:width .5s var(--ease);
}

/* Active step */
.step-indicator .si-step.active .si-num {
  background:var(--accent-solid); border-color:var(--accent-solid); color:#fff;
  box-shadow:0 0 0 4px var(--accent-soft);
}
.step-indicator .si-step.active .si-label { color:var(--ink); font-weight:650; }

/* Completed step */
.step-indicator .si-step.complete .si-num {
  background:var(--ok); border-color:var(--ok); color:#fff;
}
.step-indicator .si-step.complete .si-label { color:var(--ink-2); }
.step-indicator .si-step.complete + .si-line::after { width:100%; }

@media (max-width:640px) {
  .step-indicator { padding:var(--s-3); }
  .step-indicator .si-label { display:none; }
  .step-indicator .si-line { min-width:16px; }
}

/* ==========================================================================
   ONBOARDING — Tooltip / Popover system
   ========================================================================== */

.help-trigger {
  display:inline-flex; align-items:center; justify-content:center;
  width:18px; height:18px; border-radius:50%; flex:0 0 auto;
  background:var(--bg-sunken); border:1px solid var(--line); color:var(--ink-3);
  font-size:11px; font-weight:700; cursor:help;
  margin-left:var(--s-2); vertical-align:middle;
  transition:all .15s var(--ease);
}
.help-trigger:hover { background:var(--accent-soft); border-color:var(--accent); color:var(--accent-ink); }

.help-pop {
  position:absolute; z-index:60; top:calc(100% + 8px); left:0;
  width:280px; padding:var(--s-4);
  background:var(--bg-raised); border:1px solid var(--line);
  border-radius:var(--r-md); box-shadow:var(--sh-4);
  font-size:var(--t-sm); color:var(--ink-2); line-height:1.5;
  text-transform:none; letter-spacing:normal; font-weight:400;
  opacity:0; transform:translateY(-4px); pointer-events:none;
  transition:opacity .18s var(--ease), transform .18s var(--ease);
}
/* Click still works (ui.js), but a trigger placed immediately before its
   popover also reveals it on hover and on keyboard focus — the toolbar wants a
   glance, not a click. Inert where the two are not adjacent, as on /app/buy. */
.help-pop.visible,
.help-trigger:hover + .help-pop,
.help-trigger:focus-visible + .help-pop {
  opacity:1; transform:translateY(0); pointer-events:auto;
}
.help-pop::before {
  content:""; position:absolute; top:-6px; left:20px;
  width:12px; height:12px; background:var(--bg-raised);
  border-top:1px solid var(--line); border-left:1px solid var(--line);
  transform:rotate(45deg);
}

/* ==========================================================================
   ONBOARDING — Guided tour modal
   ========================================================================== */

.tour-overlay {
  position:fixed; inset:0; z-index:10000;
  background:rgba(8,12,20,.6);
  backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px);
  display:flex; align-items:center; justify-content:center;
  padding:var(--s-6);
  animation:scaleIn .35s var(--ease);
}
.tour-overlay[hidden] { display:none; }
.tour-card {
  background:var(--bg-raised); border-radius:var(--r-xl);
  box-shadow:var(--sh-5); max-width:520px; width:100%;
  overflow:hidden; animation:fadeInUp .4s var(--ease) both;
}
.tour-header {
  background:linear-gradient(135deg,var(--brand-800),var(--brand-500));
  padding:var(--s-8) var(--s-8) var(--s-6); color:#fff; text-align:center;
}
.tour-header h2 { color:#fff; font-size:var(--t-xl); margin-bottom:var(--s-2); }
.tour-header p { color:rgba(255,255,255,.78); font-size:var(--t-sm); margin:0; }
.tour-body { padding:var(--s-6) var(--s-8) var(--s-8); }
.tour-slides { position:relative; overflow:hidden; }
.tour-slide {
  display:none; animation:fadeInUp .35s var(--ease) both;
}
.tour-slide.active { display:block; }
.tour-slide .slide-icon {
  width:56px; height:56px; border-radius:var(--r-md);
  background:var(--accent-soft); color:var(--accent-ink);
  display:grid; place-items:center;
  margin-bottom:var(--s-4);
}
.tour-slide .slide-icon svg {
  width:27px; height:27px; display:block;
  fill:none; stroke:currentColor; stroke-width:1.6;
  stroke-linecap:round; stroke-linejoin:round;
}
.tour-slide h3 { font-size:var(--t-lg); margin-bottom:var(--s-2); }
.tour-slide p { font-size:var(--t-sm); color:var(--ink-3); margin:0; line-height:1.65; }
.tour-dots {
  display:flex; gap:var(--s-2); justify-content:center;
  margin:var(--s-6) 0 var(--s-4);
}
.tour-dots span {
  width:8px; height:8px; border-radius:50%; background:var(--line);
  transition:all .25s var(--ease); cursor:pointer;
}
.tour-dots span.on { background:var(--accent); width:24px; border-radius:var(--r-full); }
.tour-actions {
  display:flex; align-items:center; justify-content:space-between; gap:var(--s-3);
}

/* ==========================================================================
   ONBOARDING — Hero section (first-time dashboard)
   ========================================================================== */

.onboard-hero {
  position:relative; border-radius:var(--r-xl); overflow:hidden;
  padding:var(--s-16) var(--s-8); text-align:center;
  background:linear-gradient(135deg,var(--brand-950) 0%,var(--brand-800) 40%,var(--brand-600) 100%);
  background-size:200% 200%; animation:gradientShift 8s ease infinite;
  color:#fff; margin-bottom:var(--s-8);
}
.onboard-hero::before {
  content:""; position:absolute; inset:0; z-index:0;
  background:
    radial-gradient(circle at 20% 30%, rgba(94,143,250,.2) 0%, transparent 50%),
    radial-gradient(circle at 80% 70%, rgba(62,192,141,.15) 0%, transparent 45%);
  pointer-events:none;
}
.onboard-hero > * { position:relative; z-index:1; }
.onboard-hero h1 {
  color:#fff; font-size:clamp(24px,4vw,var(--t-3xl));
  max-width:18ch; margin:0 auto var(--s-3);
}
.onboard-hero .lede {
  color:rgba(255,255,255,.78); font-size:var(--t-md);
  max-width:50ch; margin:0 auto var(--s-8); line-height:1.6;
}

/* Floating hexagons (decorative, CSS only) */
.hex-float {
  position:absolute; opacity:.08; pointer-events:none; z-index:0;
}
.hex-float svg { width:100%; height:100%; }
.hex-1 { width:120px; top:10%; left:5%;  animation:floatUp 7s ease-in-out infinite; }
.hex-2 { width:80px;  top:60%; left:12%; animation:floatDown 6s ease-in-out infinite; }
.hex-3 { width:140px; top:15%; right:8%; animation:floatDown 8s ease-in-out infinite; }
.hex-4 { width:60px;  top:70%; right:15%;animation:floatUp 5s ease-in-out infinite; }
.hex-5 { width:100px; bottom:10%; left:40%; animation:floatDown 9s ease-in-out infinite; }

/* Service choice cards (on onboarding hero) */
.svc-choices {
  display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
  gap:var(--s-5); max-width:780px; margin:0 auto var(--s-8);
}
@media (max-width:700px) { .svc-choices { grid-template-columns:1fr; } }

.svc-choice {
  background:color-mix(in srgb, #fff 10%, transparent);
  backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
  border:1px solid rgba(255,255,255,.15); border-radius:var(--r-lg);
  padding:var(--s-6); text-align:left; cursor:pointer; text-decoration:none;
  display:flex; flex-direction:column; gap:var(--s-3);
  transition:all .22s var(--ease);
}
.svc-choice:hover {
  background:color-mix(in srgb, #fff 18%, transparent);
  border-color:rgba(255,255,255,.3);
  transform:translateY(-3px); box-shadow:0 12px 40px rgba(0,0,0,.2);
  text-decoration:none;
}
.svc-choice .svc-icon {
  width:44px; height:44px; border-radius:var(--r-md);
  background:rgba(255,255,255,.15); display:grid; place-items:center; color:#fff;
}
.svc-choice .svc-icon svg {
  width:22px; height:22px; display:block;
  fill:none; stroke:currentColor; stroke-width:1.6;
  stroke-linecap:round; stroke-linejoin:round;
}
.svc-choice h3 { color:#fff; font-size:var(--t-lg); margin:0; }
.svc-choice .q { color:rgba(255,255,255,.68); font-size:var(--t-sm); font-style:italic; margin:0; }
.svc-choice .svc-benefits {
  list-style:none; padding:0; margin:var(--s-2) 0 0; display:grid; gap:var(--s-2);
}
.svc-choice .svc-benefits li {
  font-size:var(--t-xs); color:rgba(255,255,255,.72);
  padding-left:var(--s-5); position:relative;
}
.svc-choice .svc-benefits li::before {
  content:"✓"; position:absolute; left:0; color:var(--ok); font-weight:700;
}
.svc-choice .svc-cta {
  margin-top:auto; padding-top:var(--s-4); border-top:1px solid rgba(255,255,255,.12);
  font-size:var(--t-sm); font-weight:650; color:#fff; display:flex; align-items:center; gap:var(--s-2);
}
.svc-choice .svc-cta::after { content:"→"; transition:transform .2s var(--ease); }
.svc-choice:hover .svc-cta::after { transform:translateX(4px); }

/* ==========================================================================
   ONBOARDING — Trust badges
   ========================================================================== */

.trust-row {
  display:flex; justify-content:center; gap:var(--s-6); flex-wrap:wrap;
  margin-bottom:var(--s-8);
}
.trust-badge {
  display:flex; align-items:center; gap:var(--s-3);
  padding:var(--s-3) var(--s-5);
  background:var(--bg-raised); border:1px solid var(--line);
  border-radius:var(--r-full); box-shadow:var(--sh-1);
  font-size:var(--t-sm); font-weight:550; color:var(--ink-2);
}
.trust-badge .tb-icon {
  width:28px; height:28px; border-radius:50%; flex:0 0 auto;
  background:var(--accent-soft); color:var(--accent-ink);
  display:grid; place-items:center;
}
.trust-badge .tb-icon svg {
  width:15px; height:15px; display:block;
  fill:none; stroke:currentColor; stroke-width:1.7;
  stroke-linecap:round; stroke-linejoin:round;
}
.trust-badge .tb-num { font-weight:700; color:var(--ink); }

/* ==========================================================================
   ONBOARDING — How it works (horizontal)
   ========================================================================== */

.how-row {
  display:grid; grid-template-columns:1fr auto 1fr auto 1fr;
  gap:0; align-items:start; max-width:780px; margin:0 auto var(--s-8);
}
@media (max-width:640px) {
  .how-row { grid-template-columns:1fr; gap:var(--s-4); }
  .how-connector { display:none; }
}
.how-step {
  text-align:center; padding:var(--s-4);
}
.how-step .how-num {
  width:44px; height:44px; border-radius:50%; margin:0 auto var(--s-3);
  background:var(--accent-soft); color:var(--accent-ink);
  display:grid; place-items:center; font-weight:700; font-size:var(--t-md);
}
.how-step h4 { margin-bottom:var(--s-1); font-size:var(--t-base); }
.how-step p { font-size:var(--t-xs); color:var(--ink-3); margin:0; }
.how-connector {
  display:flex; align-items:center; padding-top:22px;
  color:var(--ink-4); font-size:20px;
}

/* ==========================================================================
   ONBOARDING — Enhanced purchase form
   ========================================================================== */

/* The service artwork is a wide 3:2 render whose subject sits right of centre
   over a large field of flat navy. Cropping it into a side panel threw away the
   subject and left the copy stranded in an empty half-card, so the hero is one
   full-bleed banner: art underneath, copy laid over the negative space it was
   drawn to leave. The scrim keeps the copy legible in both themes without
   changing the artwork's own colour. */
.hero-banner {
  position:relative; overflow:hidden; background:var(--brand-950);
  border:1px solid var(--line); border-radius:var(--r-lg); margin-bottom:var(--s-6);
  box-shadow:var(--sh-1); isolation:isolate;
}
.hero-banner > figure { position:absolute; inset:0; margin:0; z-index:-2; }
.hero-banner > figure img {
  width:100%; height:100%; display:block; object-fit:cover; object-position:center 55%;
}
/* The artwork's own flat navy is the scrim colour, so the fade reads as part of
   the render rather than as a panel laid over it, and it must be clear by the
   two-thirds mark — past that the three renders all put their subject. */
.hero-banner::after {
  content:""; position:absolute; inset:0; z-index:-1; pointer-events:none;
  background:linear-gradient(100deg,
    rgba(20,32,74,.95) 0 20%, rgba(20,32,74,.84) 34%, rgba(20,32,74,.26) 52%, rgba(20,32,74,0) 66%);
}

.buy-hero {
  display:flex; align-items:center; gap:var(--s-5);
  min-height:clamp(240px,23vw,330px); padding:var(--s-8);
}
.buy-hero-copy { display:flex; align-items:center; gap:var(--s-5); max-width:min(600px,62%); }
.buy-layout {
  display:grid; grid-template-columns:minmax(0,1.2fr) minmax(300px,.8fr);
  align-items:start; gap:var(--s-5);
}
.buy-scope-card { min-width:0; }
.buy-summary { position:sticky; top:calc(var(--topbar-h) + var(--s-5)); }
.buy-hero .buy-icon {
  width:52px; height:52px; border-radius:var(--r-md); flex:0 0 auto;
  background:var(--accent-solid); color:#fff;
  display:grid; place-items:center;
  box-shadow:0 6px 18px rgba(0,0,0,.35);
}
.buy-hero .buy-icon svg {
  width:26px; height:26px; display:block;
  fill:none; stroke:currentColor; stroke-width:1.6;
  stroke-linecap:round; stroke-linejoin:round;
}
/* h1 here, not h2 — this is the buy step's primary heading, but it should read
   at card scale rather than the full page-title size. */
.buy-hero h1 { font-size:var(--t-2xl); margin-bottom:var(--s-2); letter-spacing:-.028em; color:#fff; }
.buy-hero p { font-size:var(--t-sm); color:rgba(255,255,255,.78); margin:0; max-width:38ch; }

/* A heading that carries an aside/panel rather than a page section. Keeps the
   document outline correct without inheriting full h2 display size. */
.side-panel-title { font-size:var(--t-md); margin-bottom:var(--s-4); }

/* A section heading that must be an h2 for the document outline but reads at
   subsection scale. */
.section-title-sm { font-size:var(--t-md); margin-bottom:var(--s-4); }

.price-card {
  background:linear-gradient(135deg,var(--brand-950),var(--brand-800));
  border-radius:var(--r-md); padding:var(--s-5); color:#fff; text-align:center;
  margin-top:var(--s-4);
}
.price-card .price-label { font-size:var(--t-xs); text-transform:uppercase; letter-spacing:.1em; color:rgba(255,255,255,.65); }
.price-card .price-amount { font-size:var(--t-2xl); font-weight:700; margin:var(--s-1) 0; }
.price-card .price-note { font-size:var(--t-xs); color:rgba(255,255,255,.6); }

.benefit-check {
  display:flex; align-items:flex-start; gap:var(--s-3);
  padding:var(--s-2) 0; font-size:var(--t-sm); color:var(--ink-2);
}
.benefit-check .bc-icon {
  width:20px; height:20px; border-radius:50%; flex:0 0 auto;
  background:var(--ok-soft); color:var(--ok);
  display:grid; place-items:center; font-size:11px; font-weight:700;
  margin-top:1px;
}

/* BrandReach has a separate purchase journey: the operator supplies verifiable
   listing information before a one-time onboarding charge is created. The
   page uses the same spatial artwork language as the report flows, while its
   wider lead column makes the different decision legible immediately. */
.integration-hero { display:flex; align-items:center; min-height:clamp(280px,26vw,380px); padding:var(--s-8); }
.integration-copy { display:flex; flex-direction:column; align-items:flex-start; max-width:min(560px,60%); }
.integration-copy h1 { max-width:17ch; font-size:var(--t-2xl); letter-spacing:-.028em; color:#fff; }
.integration-copy > p { max-width:40ch; color:rgba(255,255,255,.78); margin:var(--s-3) 0 var(--s-6); }
.integration-layout { display:grid; grid-template-columns:minmax(0,1.15fr) minmax(280px,.85fr); align-items:start; gap:var(--s-5); }
.integration-benefits h2 { font-size:var(--t-lg); margin-bottom:var(--s-5); }
.integration-benefit-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:var(--s-4); }
.integration-benefit-grid > div { min-width:0; padding-right:var(--s-4); }
.integration-benefit-grid > div:not(:last-child) { border-right:1px solid var(--line-soft); }
.integration-benefit-grid h3 { font-size:var(--t-sm); margin-bottom:var(--s-2); }
.integration-benefit-grid p,.integration-price > p { color:var(--ink-3); font-size:var(--t-sm); margin:0; }
.integration-price { background:var(--accent-soft); border-color:color-mix(in srgb, var(--accent) 22%, var(--line)); }
.integration-price-label { color:var(--accent-ink) !important; font-size:var(--t-xs) !important; font-weight:700; letter-spacing:.08em; text-transform:uppercase; }
.integration-price-amount { color:var(--ink) !important; font-size:var(--t-2xl) !important; font-weight:750; letter-spacing:-.04em; margin:var(--s-2) 0 var(--s-4) !important; }
.integration-price .muted { margin-top:var(--s-4); }
/* Narrow: there is no wide field of navy left to lay copy over, and a scrim
   sized for the copy would cover the whole card. Stack instead — art band on
   top, copy below on the card's own navy, so no overlap can occur whatever
   the copy wraps to. */
@media (max-width:860px) {
  .buy-layout,.integration-layout { grid-template-columns:1fr; }
  .buy-summary { position:static; }
  .buy-hero,.integration-hero {
    display:flex; flex-direction:column; align-items:stretch;
    min-height:0; padding:0; gap:0;
  }
  .hero-banner > figure { position:static; z-index:auto; order:-1; height:190px; }
  .hero-banner::after { content:none; }
  .buy-hero-copy,.integration-copy { max-width:100%; padding:var(--s-6); }
}
@media (max-width:620px) {
  .buy-hero-copy { align-items:flex-start; gap:var(--s-4); }
  .buy-hero .buy-icon { width:44px; height:44px; }
  .buy-hero h1 { font-size:var(--t-lg); }
  .integration-copy h1 { font-size:var(--t-xl); }
  .integration-benefit-grid { grid-template-columns:1fr; gap:var(--s-4); }
  .integration-benefit-grid > div { padding:0 0 var(--s-4); }
  .integration-benefit-grid > div:not(:last-child) { border-right:0; border-bottom:1px solid var(--line-soft); }
}

/* Budget reassurance */
.budget-hint {
  padding:var(--s-3) var(--s-4); margin-top:var(--s-2);
  background:var(--ok-soft); border-radius:var(--r);
  font-size:var(--t-xs); color:var(--ok); font-weight:550;
  display:none; animation:fadeInUp .3s var(--ease);
}
.budget-hint.visible { display:block; }

/* ==========================================================================
   ONBOARDING — Enhanced scope confirmation
   ========================================================================== */

.scope-box-premium {
  border:2px solid var(--accent); border-radius:var(--r-lg); overflow:hidden;
  background:var(--bg-raised); box-shadow:var(--sh-3);
}
.scope-box-premium .sbp-head {
  background:linear-gradient(135deg,var(--brand-800),var(--brand-500));
  padding:var(--s-4) var(--s-5); color:#fff;
  display:flex; align-items:center; gap:var(--s-3);
}
.scope-box-premium .sbp-head .lock-icon {
  width:28px; height:28px; border-radius:50%; flex:0 0 auto;
  background:rgba(255,255,255,.18); display:grid; place-items:center;
  color:#fff;
}
.scope-box-premium .sbp-head .lock-icon svg {
  width:15px; height:15px; display:block;
  fill:none; stroke:currentColor; stroke-width:1.8;
  stroke-linecap:round; stroke-linejoin:round;
}
.scope-box-premium .sbp-head span { font-size:var(--t-sm); font-weight:650; letter-spacing:.05em; text-transform:uppercase; }
.scope-box-premium .sbp-body { padding:var(--s-5); }

/* Animated checkbox */
.check-animated input:checked + span::before {
  animation:checkPop .3s var(--ease);
}

/* ==========================================================================
   ONBOARDING — Status-aware plan cards
   ========================================================================== */

.plan-premium {
  position:relative; overflow:hidden;
  transition:transform .2s var(--ease), box-shadow .2s var(--ease), border-color .2s var(--ease);
}
.plan-premium:hover {
  transform:translateY(-2px); box-shadow:var(--sh-3);
}

/* Status accents */
.plan-premium.status-active {
  border-color:var(--accent);
  animation:pulseGlow 2.5s ease-in-out infinite;
}
.plan-premium.status-active::before {
  content:""; position:absolute; top:0; left:0; right:0; height:3px;
  background:linear-gradient(90deg,var(--brand-400),var(--brand-600));
}
.plan-premium.status-running::before {
  content:""; position:absolute; top:0; left:0; right:0; height:3px;
  background:linear-gradient(90deg,var(--brand-400),var(--brand-600));
  animation:drawLine 2s ease-in-out infinite;
}
/* Delivered is the resting state and will be most of the cards on screen. It
   keeps the top status rule but not the full-perimeter tint, so the eye still
   goes to the cards that want something from the user (active / failed). */
.plan-premium.status-consumed::before {
  content:""; position:absolute; top:0; left:0; right:0; height:3px;
  background:var(--ok);
}
.plan-premium.status-failed {
  border-color:var(--bad);
}
.plan-premium.status-failed::before {
  content:""; position:absolute; top:0; left:0; right:0; height:3px;
  background:var(--bad);
}

.plan-next-action {
  margin-top:var(--s-2); padding:var(--s-3) var(--s-4);
  background:var(--accent-soft); border-radius:var(--r);
  font-size:var(--t-xs); color:var(--accent-ink);
}

/* ==========================================================================
   ONBOARDING — Premium auth pages
   ========================================================================== */

.auth-aside-premium {
  position:relative; overflow:hidden;
}

/* One composed illustration gives the sign-in surface a human, specific point
   of view. It intentionally sits behind the message rather than behaving like
   a second interface. */
.auth-illustration { position:absolute; inset:0; z-index:0; pointer-events:none; overflow:hidden; }
.auth-illustration img {
  width:100%; height:100%; display:block; object-fit:cover; object-position:center;
  opacity:.96; transform:scale(1.015);
}
.auth-aside-premium > *:not(.auth-illustration) { position:relative; z-index:2; }
/* A vignette keeps the headline readable while allowing the illustrated city
   to be discovered at the open edge beside the form. */
.auth-aside-premium::after {
  content:""; position:absolute; inset:0; z-index:1; pointer-events:none;
  background:
    linear-gradient(104deg,
      rgba(8,18,40,.88) 0%, rgba(8,18,40,.62) 37%,
      rgba(8,18,40,.12) 70%, rgba(8,18,40,.04) 100%),
    linear-gradient(180deg, rgba(8,18,40,.36), rgba(8,18,40,0) 31%, rgba(8,18,40,.52));
}

.auth-feature {
  display:flex; align-items:flex-start; gap:var(--s-3);
  margin-bottom:var(--s-4);
}
.auth-feature .af-icon {
  width:36px; height:36px; border-radius:var(--r); flex:0 0 auto;
  background:rgba(255,255,255,.1); border:1px solid rgba(255,255,255,.14);
  display:grid; place-items:center; color:#fff;
}
.auth-feature .af-icon svg { width:19px; height:19px; display:block; }
.auth-feature h4 { color:#fff; font-size:var(--t-sm); margin-bottom:2px; }
.auth-feature p { color:rgba(255,255,255,.6); font-size:var(--t-xs); margin:0; }

.auth-stat-badge {
  display:inline-flex; align-items:center; gap:var(--s-2);
  padding:var(--s-2) var(--s-4); border-radius:var(--r-full);
  background:rgba(255,255,255,.1); border:1px solid rgba(255,255,255,.15);
  font-size:var(--t-xs); color:rgba(255,255,255,.85); font-weight:600;
  margin-top:var(--s-5);
}
.auth-stat-badge .asb-dot {
  width:6px; height:6px; border-radius:50%; background:#7fd7b4;
}

/* Password strength meter (enhanced) */
.pw-meter-wrap { margin-top:var(--s-1); }
.pw-meter-label { font-size:var(--t-xs); color:var(--ink-3); margin-top:2px; }

/* Form field with icon */
.field-with-icon { position:relative; }
.field-with-icon .fi-icon {
  position:absolute; left:var(--s-3); top:50%; transform:translateY(-50%);
  color:var(--ink-4); font-size:var(--t-sm); pointer-events:none; z-index:1;
}
.field-with-icon input,
.field-with-icon select { padding-left:var(--s-8); }

/* ==========================================================================
   ONBOARDING — Return visitor refinements
   ========================================================================== */

.greeting-anim {
  animation:fadeInDown .5s var(--ease) both;
}

.quick-actions {
  display:flex; gap:var(--s-3); flex-wrap:wrap; margin-bottom:var(--s-6);
}

/* Support tickets deliberately reuse the app's restrained cards and type scale.
   The thread needs enough contrast to scan who said what, without looking like
   a separate chat product embedded in the dashboard. */
.support-layout { align-items:start; }
.support-section-title { font-size:var(--t-lg); margin-bottom:var(--s-4); }
.support-form { display:grid; gap:var(--s-4); }
.support-field { display:grid; gap:var(--s-2); }
.support-field label { color:var(--ink-2); font-size:var(--t-sm); font-weight:650; }
.support-field textarea { resize:vertical; min-height:118px; }
.support-status-field { max-width:260px; }
.form-error { margin:0; color:var(--bad); font-size:var(--t-xs); }
.support-ticket-list { display:grid; }
.support-ticket-row {
  display:flex; align-items:center; justify-content:space-between; gap:var(--s-4);
  padding:var(--s-4) var(--s-5); color:inherit; border-bottom:1px solid var(--line-soft);
  transition:background .14s var(--ease);
}
.support-ticket-row:last-child { border-bottom:0; }
.support-ticket-row:hover { background:var(--bg-sunken); text-decoration:none; }
.support-ticket-row strong { display:block; color:var(--ink); }
.support-ticket-meta { display:block; color:var(--ink-3); font-size:var(--t-xs); margin-top:var(--s-1); }
.support-thread { overflow:hidden; }
.support-message { padding:var(--s-5); border-bottom:1px solid var(--line-soft); }
.support-message:last-child { border-bottom:0; }
.support-message-staff { background:var(--accent-soft); }
.support-message-meta { display:flex; justify-content:space-between; gap:var(--s-3); color:var(--ink-3); font-size:var(--t-xs); margin-bottom:var(--s-3); }
.support-message-meta strong { color:var(--ink-2); }
.support-message-body { color:var(--ink); line-height:1.65; overflow-wrap:anywhere; }
.support-reply { margin-top:var(--s-5); }
.support-back { margin-top:var(--s-5); font-size:var(--t-sm); }
.support-filters { display:flex; flex-wrap:wrap; gap:var(--s-2); margin:0 0 var(--s-5); }
@media (max-width:640px) {
  .support-ticket-row { align-items:flex-start; flex-direction:column; }
  .support-message-meta { flex-direction:column; gap:var(--s-1); }
}
.quick-action {
  display:flex; align-items:center; gap:var(--s-3);
  padding:var(--s-3) var(--s-5); border-radius:var(--r-full);
  background:var(--bg-raised); border:1px solid var(--line);
  font-size:var(--t-sm); font-weight:550; color:var(--ink-2);
  text-decoration:none; transition:all .15s var(--ease);
  box-shadow:var(--sh-1);
}
.quick-action:hover {
  border-color:var(--accent); color:var(--accent-ink);
  transform:translateY(-1px); box-shadow:var(--sh-2);
  text-decoration:none;
}
.quick-action .qa-icon {
  width:24px; height:24px; border-radius:50%; flex:0 0 auto;
  background:var(--accent-soft); color:var(--accent-ink);
  display:grid; place-items:center; font-size:11px; font-weight:700;
}
.quick-action .qa-icon-svg svg {
  width:13px; height:13px; display:block;
  fill:none; stroke:currentColor; stroke-width:1.8;
  stroke-linecap:round; stroke-linejoin:round;
}

/* ==========================================================================
   DATA & COVERAGE DASHBOARD
   ========================================================================== */

.data-hero {
  display:flex; align-items:flex-end; justify-content:space-between; gap:var(--s-8);
  padding:var(--s-8); margin-bottom:var(--s-8); border:1px solid var(--line);
  border-radius:var(--r-xl); background:
    radial-gradient(circle at 92% 4%,var(--accent-soft),transparent 33%), var(--bg-raised);
}
.data-hero > div:first-child { max-width:680px; }
.data-hero h1 { font-size:clamp(28px,4vw,var(--t-3xl)); margin-bottom:var(--s-3); }
.data-hero p:last-child { color:var(--ink-3); font-size:var(--t-md); line-height:1.65; margin:0; }
.data-hero-status { min-width:220px; display:flex; align-items:flex-start; gap:var(--s-3); padding:var(--s-4); border:1px solid var(--line-soft); border-radius:var(--r-md); background:var(--bg-sunken); }
.data-hero-status strong,.data-hero-status .tiny { display:block; }
.data-hero-status strong { font-size:var(--t-sm); margin:2px 0; color:var(--ink); }
.data-status-dot { width:9px; height:9px; flex:0 0 auto; border-radius:50%; margin-top:6px; background:var(--ink-4); }
.data-status-dot.is-good { background:var(--ok); box-shadow:0 0 0 4px var(--ok-soft); }
.data-status-dot.is-warn { background:var(--warn); box-shadow:0 0 0 4px var(--warn-soft); }
/* `.section-label-row`, `.card-heading`, `.metric-bar`, `.tier-row` and
   `.tier-bars` were defined here; they are shared primitives now — see
   "Naming a band of content" under Primitives — surfaces. */
.data-kpi-row { margin-bottom:var(--s-8); }
.data-insight-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); align-items:stretch; gap:var(--s-5); margin-bottom:var(--s-8); }
.source-comparison { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:var(--s-3); margin-top:var(--s-5); }
.source-card { padding:var(--s-4); border:1px solid var(--line-soft); border-radius:var(--r); background:var(--bg-sunken); }
.source-card-top { display:flex; align-items:center; gap:var(--s-3); }
.source-card-top h3 { font-size:var(--t-sm); }
.source-card-top p { color:var(--ink-3); font-size:var(--t-xs); margin:1px 0 0; }
.source-icon { width:28px; height:28px; flex:0 0 auto; display:grid; place-items:center; border-radius:var(--r-sm); background:var(--accent-soft); color:var(--accent-ink); font-size:var(--t-xs); font-weight:750; }
.source-icon-field { background:var(--ok-soft); color:var(--ok); }
.source-stat { display:flex; align-items:baseline; gap:var(--s-2); margin:var(--s-5) 0 var(--s-2); }
.source-stat strong { font-size:var(--t-xl); letter-spacing:-.03em; }
.source-stat span { color:var(--ink-3); font-size:var(--t-xs); }
.source-card .metric-bar { background:var(--bg-raised); }
.source-card .tiny { margin:var(--s-2) 0 0; }
.metric-bar-tier > i { background:linear-gradient(90deg,var(--accent),var(--brand-300)); }
.city-coverage-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(250px,1fr)); gap:var(--s-4); margin-bottom:var(--s-8); }
.city-coverage-card { display:flex; flex-direction:column; gap:var(--s-4); padding:var(--s-5); border:1px solid var(--line); border-radius:var(--r-md); background:var(--bg-raised); box-shadow:var(--sh-1); transition:transform .16s var(--ease),box-shadow .16s var(--ease),border-color .16s var(--ease); }
.city-coverage-card:hover { transform:translateY(-2px); border-color:var(--brand-300); box-shadow:var(--sh-3); }
.city-coverage-art { height:128px; overflow:hidden; margin:calc(var(--s-5) * -1) calc(var(--s-5) * -1) 0; background:var(--accent-soft); border-radius:var(--r-md) var(--r-md) 0 0; }
.city-coverage-art img { width:100%; height:100%; object-fit:cover; display:block; }
.city-card-head { display:flex; align-items:flex-start; justify-content:space-between; gap:var(--s-3); }
.city-card-head h3 { font-size:var(--t-md); }
.city-card-head p { color:var(--ink-3); font-size:var(--t-xs); margin:2px 0 0; }
.city-card-head > strong { color:var(--accent-ink); font-size:var(--t-xl); letter-spacing:-.03em; }
.metric-bar-city { height:8px; background:var(--accent-soft); }
.city-card-meta { display:flex; gap:var(--s-3); flex-wrap:wrap; color:var(--ink-3); font-size:var(--t-xs); }
.city-card-meta b { color:var(--ink); }
.city-coverage-card .btn { align-self:flex-start; margin-top:auto; }
.data-empty { min-height:150px; display:flex; align-items:center; justify-content:center; gap:var(--s-4); padding:var(--s-6); border:1px dashed var(--line); border-radius:var(--r-md); color:var(--ink-3); text-align:center; }
.data-empty p { max-width:42ch; margin:0; font-size:var(--t-sm); }
.city-empty { grid-column:1 / -1; }
.city-empty h3 { margin-bottom:var(--s-1); }
.data-insight-grid-bottom { margin-bottom:var(--s-6); }
.outcome-score-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:var(--s-3); margin:var(--s-5) 0; }
.outcome-score { display:flex; align-items:center; gap:var(--s-3); padding:var(--s-3); background:var(--bg-sunken); border:1px solid var(--line-soft); border-radius:var(--r); }
.score-ring { --ring-fill:var(--accent); width:54px; height:54px; flex:0 0 auto; display:grid; place-items:center; border-radius:50%; background:conic-gradient(var(--ring-fill) calc(var(--score) * 1%),var(--line-soft) 0); position:relative; }
.score-ring::before { content:""; position:absolute; inset:6px; border-radius:inherit; background:var(--bg-sunken); }
.score-ring strong { position:relative; z-index:1; font-size:var(--t-xs); }
.score-ring-secondary { --ring-fill:var(--ok); }
.outcome-score h3 { font-size:var(--t-sm); }
.outcome-score p { color:var(--ink-3); font-size:var(--t-xs); margin:1px 0 0; }
.outcome-list { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); padding-top:var(--s-4); border-top:1px solid var(--line-soft); gap:var(--s-3); color:var(--ink-3); font-size:var(--t-xs); }
.outcome-list b { display:block; color:var(--ink); font-size:var(--t-sm); margin-top:2px; }
.locality-list { display:grid; gap:var(--s-3); margin-top:var(--s-5); }
.locality-row { display:grid; grid-template-columns:minmax(0,1fr) minmax(130px,.65fr); align-items:center; gap:var(--s-4); }
.locality-row strong,.locality-row span { display:block; }
.locality-row strong { font-size:var(--t-sm); }
.locality-row > div > span { margin-top:1px; color:var(--ink-3); font-size:var(--t-xs); }
.locality-meter { display:grid; grid-template-columns:minmax(0,1fr) 34px; align-items:center; gap:var(--s-2); }
.locality-meter > span { color:var(--ink-3); font-size:var(--t-xs); text-align:right; }
.data-detail { border:1px solid var(--line); border-radius:var(--r-md); background:var(--bg-raised); overflow:hidden; }
.data-detail summary { padding:var(--s-4) var(--s-5); cursor:pointer; color:var(--ink-2); font-size:var(--t-sm); font-weight:650; }
.data-detail[open] summary { border-bottom:1px solid var(--line-soft); }
.data-detail .table-wrap { margin:0; }

@media (max-width:860px) {
  .data-hero { align-items:flex-start; flex-direction:column; }
  .data-insight-grid { grid-template-columns:1fr; }
}
@media (max-width:560px) {
  .data-hero { padding:var(--s-5); margin-bottom:var(--s-6); }
  .data-hero-status { min-width:0; width:100%; }
  .section-label-row { align-items:flex-start; flex-direction:column; }
  .source-comparison,.outcome-score-grid { grid-template-columns:1fr; }
  .tier-row { grid-template-columns:minmax(0,1fr) 80px 32px; gap:var(--s-2); }
  .outcome-list { grid-template-columns:1fr; gap:var(--s-2); }
  .outcome-list b { display:inline; margin:0 0 0 var(--s-1); }
  .locality-row { grid-template-columns:1fr; gap:var(--s-2); }
}

/* Proximity graph: the node dots carry meaning (degree), so the legend needs a
   size ramp as well as the colour swatches .legend already styles. */
.legend .dot { border-radius:50%; background:var(--accent); flex:0 0 auto; }
#prox-detail { font-size:var(--t-sm); }
#prox-detail table { width:100%; }

/* ==========================================================================
   Animated icons
   --------------------------------------------------------------------------
   The dashboard's icons are hand-drawn Lucide geometry inlined into the Django
   templates. animateicons.in and animate-ui ship the same shapes as React
   components animated with Motion, which this app cannot render — there is no
   React here and adding a bundler for icons would be a poor trade. The motion
   itself is the valuable part and it is reproducible in CSS, so each icon is
   tagged `data-icon="<lucide-name>"` and given a behaviour that matches what
   the shape does: a pin drops, a gear turns, a lock lifts its shackle.

   Restraint is deliberate. This is an analytical product a customer takes to a
   lender, so the motion is short, hover-triggered, and never idles. The global
   `prefers-reduced-motion` block above already reduces every duration to
   0.01ms, so nothing here needs its own opt-out.
   ========================================================================== */

[data-icon] > * {
  /* SVG transforms are relative to the viewBox origin unless told otherwise,
     so a rotate() without this swings the shape out of frame instead of
     turning it in place. */
  transform-box: fill-box;
  transform-origin: center;
}

:where(a, button, label, .card, .feature, .svc-card, .tb-item, .slide, .qa-card,
        .scope-bar, .lock, .sbp-lock):hover [data-icon] > *,
[data-icon]:hover > * {
  animation-duration: .45s;
  animation-timing-function: var(--ease);
  animation-fill-mode: both;
}

/* -- stagger helpers: children animate in reading order ------------------- */
[data-icon] > *:nth-child(2) { animation-delay: .05s; }
[data-icon] > *:nth-child(3) { animation-delay: .10s; }
[data-icon] > *:nth-child(4) { animation-delay: .15s; }

/* -- per-icon behaviour --------------------------------------------------- */

/* Dashboard: the four panes pop in sequence. */
:where(a, button, .card):hover [data-icon="grid"] > *,
[data-icon="grid"]:hover > * { animation-name: ic-pop; }

/* A pin drops onto the map and settles. */
:where(a, button, label, .card, .svc-card, .tb-item, .slide):hover [data-icon="map-pin"] > *,
[data-icon="map-pin"]:hover > * { animation-name: ic-drop; }

/* A roof settles onto its walls. */
:where(a, button, label, .card, .svc-card, .tb-item, .slide):hover [data-icon="home"] > *,
[data-icon="home"]:hover > * { animation-name: ic-settle; }

/* A star turns as it catches the light. */
:where(a, button, .card):hover [data-icon="star"] > *,
[data-icon="star"]:hover > * { animation-name: ic-turn; animation-duration: .55s; }

/* Table rules sweep across. */
:where(a, button, .card, .tb-item):hover [data-icon="table"] > * + *,
[data-icon="table"]:hover > * + * { animation-name: ic-sweep; }

/* Clipboard and document lines write on, one after another. */
:where(a, button, .card, .icon):hover [data-icon="clipboard"] > * + * + *,
:where(a, button, .card, .qa-card, .slide):hover [data-icon="file-text"] > * + * + *,
[data-icon="clipboard"]:hover > * + * + *,
[data-icon="file-text"]:hover > * + * + * { animation-name: ic-write; }

/* The magnetic stripe slides. */
:where(a, button, .card):hover [data-icon="credit-card"] > * + *,
[data-icon="credit-card"]:hover > * + * { animation-name: ic-sweep; }

/* A head nods. */
:where(a, button, .card):hover [data-icon="user"] > *:first-child,
[data-icon="user"]:hover > *:first-child { animation-name: ic-nod; }

/* The question mark asks again. */
:where(a, button, .card):hover [data-icon="help-circle"] > * + *,
[data-icon="help-circle"]:hover > * + * { animation-name: ic-nod; }

/* An envelope flap dips open. */
:where(a, button, .card):hover [data-icon="mail"] > * + *,
[data-icon="mail"]:hover > * + * { animation-name: ic-dip; }

/* Gears and suns turn; the moon only tilts, because a spinning moon is silly. */
:where(a, button, .card):hover [data-icon="settings"] > *,
[data-icon="settings"]:hover > * { animation-name: ic-rotate; animation-duration: .6s; }
:where(a, button, .card):hover [data-icon="sun"] > *,
[data-icon="sun"]:hover > * { animation-name: ic-rotate; animation-duration: .6s; }
:where(a, button, .card):hover [data-icon="moon"] > *,
[data-icon="moon"]:hover > * { animation-name: ic-tilt; }

/* A box turns to show another face. */
:where(a, button, .card, .slide, .af-item):hover [data-icon="box"] > *,
[data-icon="box"]:hover > * { animation-name: ic-tilt; animation-duration: .5s; }

/* The trend line and the shield draw themselves. */
:where(a, button, .card, .icon, .tb-item, .slide, .af-item):hover [data-icon="trending-up"] > *:last-child,
[data-icon="trending-up"]:hover > *:last-child { animation-name: ic-draw; animation-duration: .6s; }
:where(a, button, .card, .tb-item):hover [data-icon="shield"] > *,
[data-icon="shield"]:hover > * { animation-name: ic-draw; animation-duration: .6s; }

/* A shackle lifts. */
:where(a, button, .card, .af-item, .scope-bar, .lock, .sbp-lock):hover [data-icon="lock"] > * + *,
[data-icon="lock"]:hover > * + * { animation-name: ic-lift; }

/* Cells and nodes come up in sequence — the same reading order the map uses. */
:where(a, button, .card, .map-empty):hover [data-icon="hexagons"] > *,
:where(a, button, .card, .map-empty):hover [data-icon="network"] > *,
:where(a, button, .card, .map-empty):hover [data-icon="fabric"] > *,
[data-icon="hexagons"]:hover > *,
[data-icon="network"]:hover > *,
[data-icon="fabric"]:hover > * { animation-name: ic-pop; }

/* -- keyframes ------------------------------------------------------------ */

@keyframes ic-pop {
  0%   { transform: scale(1); }
  45%  { transform: scale(.78); }
  100% { transform: scale(1); }
}
@keyframes ic-drop {
  0%   { transform: translateY(-3px); opacity: .35; }
  60%  { transform: translateY(1px); opacity: 1; }
  100% { transform: translateY(0); opacity: 1; }
}
@keyframes ic-settle {
  0%   { transform: translateY(-2px); }
  65%  { transform: translateY(.5px); }
  100% { transform: translateY(0); }
}
@keyframes ic-turn {
  0%   { transform: rotate(0) scale(1); }
  55%  { transform: rotate(36deg) scale(1.12); }
  100% { transform: rotate(72deg) scale(1); }
}
@keyframes ic-sweep {
  0%   { transform: translateX(-2.5px); opacity: .3; }
  100% { transform: translateX(0); opacity: 1; }
}
@keyframes ic-write {
  0%   { transform: scaleX(.2); opacity: 0; transform-origin: left center; }
  100% { transform: scaleX(1); opacity: 1; transform-origin: left center; }
}
@keyframes ic-nod {
  0%   { transform: translateY(0); }
  40%  { transform: translateY(-1.6px); }
  100% { transform: translateY(0); }
}
@keyframes ic-dip {
  0%   { transform: translateY(0); }
  45%  { transform: translateY(1.6px); }
  100% { transform: translateY(0); }
}
@keyframes ic-rotate {
  0%   { transform: rotate(0); }
  100% { transform: rotate(90deg); }
}
@keyframes ic-tilt {
  0%   { transform: rotate(0); }
  50%  { transform: rotate(-12deg); }
  100% { transform: rotate(0); }
}
@keyframes ic-lift {
  0%   { transform: translateY(0); }
  45%  { transform: translateY(-1.8px); }
  100% { transform: translateY(0); }
}
@keyframes ic-draw {
  0%   { stroke-dasharray: 44; stroke-dashoffset: 44; }
  100% { stroke-dasharray: 44; stroke-dashoffset: 0; }
}

/* Image system: conceptual art or local-city context only. These regions sit
   beside factual content and disappear cleanly when a city has no asset. */
.onboard-hero-intro { display:grid; grid-template-columns:minmax(0,1fr) minmax(260px,.9fr); gap:var(--s-6); align-items:center; max-width:1000px; margin:0 auto var(--s-8); text-align:left; }
.onboard-hero-copy h1 { margin:0 0 var(--s-3); }
.onboard-hero-copy .lede { margin:0; }
.onboard-hero-art { min-height:218px; overflow:hidden; margin:0; border:1px solid rgba(255,255,255,.16); border-radius:var(--r-lg); background:rgba(255,255,255,.06); box-shadow:0 18px 40px rgba(4,18,40,.22); }
.onboard-hero-art img { width:100%; height:100%; min-height:218px; display:block; object-fit:cover; object-position:center; }
.report-city-strip { height:180px; overflow:hidden; margin:0 0 var(--s-5); border:1px solid var(--line); border-radius:var(--r-lg); background:var(--accent-soft); }
.report-city-strip img { display:block; width:100%; height:100%; object-fit:cover; object-position:center 48%; opacity:.86; }
.pricing-hero { display:grid; grid-template-columns:minmax(0,1fr) minmax(280px,.72fr); gap:var(--s-8); align-items:center; text-align:left; max-width:1060px; margin:0 auto; }
.pricing-hero h1 { max-width:11ch; margin-inline:0; }
.pricing-hero .lede { margin-inline:0; }
.pricing-hero-art { margin:0; overflow:hidden; border:1px solid var(--line); border-radius:var(--r-lg); background:var(--accent-soft); }
.pricing-hero-art img { width:100%; height:auto; display:block; }
.franchise-landing-hero { display:grid; grid-template-columns:minmax(0,1fr) minmax(280px,.75fr); gap:var(--s-8); align-items:center; max-width:1100px; margin:0 auto; text-align:left; }
.franchise-landing-hero h1,.franchise-landing-hero .lede { margin-inline:0; }
.public-service-hero { display:grid; grid-template-columns:minmax(0,1fr) minmax(280px,.75fr); gap:var(--s-8); align-items:center; max-width:1100px; margin:0 auto; text-align:left; }
.public-service-hero h1,.public-service-hero .lede { margin-inline:0; }
.public-service-hero-art { margin:0; overflow:hidden; border:1px solid var(--line); border-radius:var(--r-lg); background:var(--accent-soft); }
.public-service-hero-art img { width:100%; height:auto; display:block; }
.franchise-landing-art { margin:0; overflow:hidden; border:1px solid var(--line); border-radius:var(--r-lg); background:var(--accent-soft); }
.franchise-landing-art img { width:100%; height:auto; display:block; }
.application-context-art { height:156px; overflow:hidden; margin:0 0 var(--s-6); border:1px solid var(--line); border-radius:var(--r-lg); background:var(--accent-soft); }
.application-context-art img { width:100%; height:100%; object-fit:cover; object-position:center 45%; display:block; opacity:.86; }
@media (max-width:760px) { .pricing-hero,.franchise-landing-hero,.public-service-hero { grid-template-columns:1fr; gap:var(--s-5); text-align:center; } .pricing-hero h1,.pricing-hero .lede,.franchise-landing-hero h1,.franchise-landing-hero .lede,.public-service-hero h1,.public-service-hero .lede { margin-inline:auto; } .franchise-landing-hero .row,.public-service-hero .hero-cta { justify-content:center; } .report-city-strip { height:132px; } }
@media (max-width:700px) { .onboard-hero-intro { grid-template-columns:1fr; gap:var(--s-5); margin-bottom:var(--s-6); } .onboard-hero-copy { text-align:center; } .onboard-hero-copy h1 { margin-inline:auto; } .onboard-hero-copy .lede { margin-inline:auto; } .onboard-hero-art { min-height:172px; } .onboard-hero-art img { min-height:172px; } }

/* Workspace card headings, matched to /app/data's panel heading.
   Scoped through `[data-panel]`, which only the workspace emits, so the shared
   `.card-head` used by other pages is left where it is. */
[data-panel] .card-head { padding:var(--s-5); }
[data-panel] .card-head h3 { font-size:var(--t-lg); }

/* One section rhythm inside a workspace panel.
 *
 * `/app/data` separates every top-level block by --s-8 and nothing else, which
 * is why it reads as calm. A workspace panel had four values and no rule: the
 * gaps measured 16, 20, 20 and 0 on Executive, 20 then 16 on the H3 map, and 0
 * on Comparison and Franchise Explorer — two cards with their borders touching,
 * which reads as one card that failed to draw. The rhythm belongs to the panel,
 * so a block cannot bring its own margin and a new block cannot forget one. */
[data-panel] > * { margin-top:0; margin-bottom:0; }
[data-panel] > * + *:not([hidden]) { margin-top:var(--s-8); }
/* The toolbar carries a margin for its other homes; inside a panel the rhythm
   rule owns the gap below it.
 *
 * Placed last on purpose: .ask-box, .kpi-row and .card all carry their own
 * margins for their other homes, and every one of them matches this rule's
 * specificity. Order is what settles it. */
[data-panel] > .toolbar { margin-bottom:0; }
